)}
-
+
{isCollapsible && (
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html
index 4b3014fd28a51..625227be3c2d2 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html
+++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html
@@ -83,8 +83,8 @@
{{ editSection.title }}
@@ -120,7 +120,7 @@
{
- const node = document.getElementById(REACT_SOURCE_FILTERS_DOM_ELEMENT_ID);
- if (!node) {
- return;
- }
-
- render(
-
- {
- $scope.editSections = $scope.editSectionsProvider(
- $scope.indexPattern,
- $scope.fieldFilter,
- $scope.indexPatternListProvider
- );
- $scope.refreshFilters();
- $scope.$apply();
- }}
- />
- ,
- node
- );
- });
- } else {
- destroySourceFiltersTable();
- }
+const TAB_INDEXED_FIELDS = 'indexedFields';
+const TAB_SCRIPTED_FIELDS = 'scriptedFields';
+const TAB_SOURCE_FILTERS = 'sourceFilters';
+
+function updateSourceFiltersTable($scope) {
+ $scope.$$postDigest(() => {
+ const node = document.getElementById(REACT_SOURCE_FILTERS_DOM_ELEMENT_ID);
+ if (!node) {
+ return;
+ }
+
+ render(
+
+ {
+ $scope.editSections = $scope.editSectionsProvider(
+ $scope.indexPattern,
+ $scope.fieldFilter,
+ $scope.indexPatternListProvider
+ );
+ $scope.refreshFilters();
+ $scope.$apply();
+ }}
+ />
+ ,
+ node
+ );
+ });
}
function destroySourceFiltersTable() {
@@ -82,44 +84,40 @@ function destroySourceFiltersTable() {
node && unmountComponentAtNode(node);
}
-function updateScriptedFieldsTable($scope, $state) {
- if ($state.tab === 'scriptedFields') {
- $scope.$$postDigest(() => {
- const node = document.getElementById(REACT_SCRIPTED_FIELDS_DOM_ELEMENT_ID);
- if (!node) {
- return;
- }
-
- render(
-
- {
- $scope.kbnUrl.redirectToRoute(obj, route);
- $scope.$apply();
- },
- getRouteHref: (obj, route) => $scope.kbnUrl.getRouteHref(obj, route),
- }}
- onRemoveField={() => {
- $scope.editSections = $scope.editSectionsProvider(
- $scope.indexPattern,
- $scope.fieldFilter,
- $scope.indexPatternListProvider
- );
- $scope.refreshFilters();
+function updateScriptedFieldsTable($scope) {
+ $scope.$$postDigest(() => {
+ const node = document.getElementById(REACT_SCRIPTED_FIELDS_DOM_ELEMENT_ID);
+ if (!node) {
+ return;
+ }
+
+ render(
+
+ {
+ $scope.kbnUrl.changeToRoute(obj, route);
$scope.$apply();
- }}
- />
- ,
- node
- );
- });
- } else {
- destroyScriptedFieldsTable();
- }
+ },
+ getRouteHref: (obj, route) => $scope.kbnUrl.getRouteHref(obj, route),
+ }}
+ onRemoveField={() => {
+ $scope.editSections = $scope.editSectionsProvider(
+ $scope.indexPattern,
+ $scope.fieldFilter,
+ $scope.indexPatternListProvider
+ );
+ $scope.refreshFilters();
+ $scope.$apply();
+ }}
+ />
+ ,
+ node
+ );
+ });
}
function destroyScriptedFieldsTable() {
@@ -127,37 +125,33 @@ function destroyScriptedFieldsTable() {
node && unmountComponentAtNode(node);
}
-function updateIndexedFieldsTable($scope, $state) {
- if ($state.tab === 'indexedFields') {
- $scope.$$postDigest(() => {
- const node = document.getElementById(REACT_INDEXED_FIELDS_DOM_ELEMENT_ID);
- if (!node) {
- return;
- }
-
- render(
-
- {
- $scope.kbnUrl.redirectToRoute(obj, route);
- $scope.$apply();
- },
- getFieldInfo: $scope.getFieldInfo,
- }}
- />
- ,
- node
- );
- });
- } else {
- destroyIndexedFieldsTable();
- }
+function updateIndexedFieldsTable($scope) {
+ $scope.$$postDigest(() => {
+ const node = document.getElementById(REACT_INDEXED_FIELDS_DOM_ELEMENT_ID);
+ if (!node) {
+ return;
+ }
+
+ render(
+
+ {
+ $scope.kbnUrl.changeToRoute(obj, route);
+ $scope.$apply();
+ },
+ getFieldInfo: $scope.getFieldInfo,
+ }}
+ />
+ ,
+ node
+ );
+ });
}
function destroyIndexedFieldsTable() {
@@ -165,6 +159,24 @@ function destroyIndexedFieldsTable() {
node && unmountComponentAtNode(node);
}
+function handleTabChange($scope, newTab) {
+ destroyIndexedFieldsTable();
+ destroySourceFiltersTable();
+ destroyScriptedFieldsTable();
+ updateTables($scope, newTab);
+}
+
+function updateTables($scope, currentTab) {
+ switch (currentTab) {
+ case TAB_SCRIPTED_FIELDS:
+ return updateScriptedFieldsTable($scope);
+ case TAB_INDEXED_FIELDS:
+ return updateIndexedFieldsTable($scope);
+ case TAB_SOURCE_FILTERS:
+ return updateSourceFiltersTable($scope);
+ }
+}
+
uiRoutes.when('/management/kibana/index_patterns/:indexPatternId', {
template,
k7Breadcrumbs: getEditBreadcrumbs,
@@ -187,10 +199,36 @@ uiModules
Promise,
config,
Private,
- AppState,
confirmModal
) {
- const $state = ($scope.state = new AppState());
+ const {
+ startSyncingState,
+ stopSyncingState,
+ setCurrentTab,
+ getCurrentTab,
+ state$,
+ } = createEditIndexPatternPageStateContainer({
+ useHashedUrl: config.get('state:storeInSessionStorage'),
+ defaultTab: TAB_INDEXED_FIELDS,
+ });
+
+ $scope.getCurrentTab = getCurrentTab;
+ $scope.setCurrentTab = setCurrentTab;
+
+ const stateChangedSub = subscribeWithScope($scope, state$, {
+ next: ({ tab }) => {
+ handleTabChange($scope, tab);
+ },
+ });
+
+ handleTabChange($scope, getCurrentTab()); // setup initial tab depending on initial tab state
+
+ startSyncingState(); // starts syncing state between state container and url
+
+ const destroyState = () => {
+ stateChangedSub.unsubscribe();
+ stopSyncingState();
+ };
$scope.fieldWildcardMatcher = (...args) =>
fieldWildcardMatcher(...args, config.get('metaFields'));
@@ -219,8 +257,6 @@ uiModules
);
$scope.refreshFilters();
$scope.fields = $scope.indexPattern.getNonScriptedFields();
- updateIndexedFieldsTable($scope, $state);
- updateScriptedFieldsTable($scope, $state);
});
$scope.refreshFilters = function() {
@@ -242,18 +278,6 @@ uiModules
$scope[filter] = val || ''; // null causes filter to check for null explicitly
};
- $scope.changeTab = function(obj) {
- $state.tab = obj.index;
- updateIndexedFieldsTable($scope, $state);
- updateScriptedFieldsTable($scope, $state);
- updateSourceFiltersTable($scope, $state);
- $state.save();
- };
-
- $scope.$watch('state.tab', function(tab) {
- if (!tab) $scope.changeTab($scope.editSections[0]);
- });
-
$scope.$watchCollection('indexPattern.fields', function() {
$scope.conflictFields = $scope.indexPattern.fields.filter(field => field.type === 'conflict');
});
@@ -329,37 +353,33 @@ uiModules
$scope.fieldFilter,
managementSetup.indexPattern.list
);
+
if ($scope.fieldFilter === undefined) {
return;
}
- switch ($state.tab) {
- case 'indexedFields':
- updateIndexedFieldsTable($scope, $state);
- case 'scriptedFields':
- updateScriptedFieldsTable($scope, $state);
- case 'sourceFilters':
- updateSourceFiltersTable($scope, $state);
- }
+ updateTables($scope, getCurrentTab());
});
$scope.$watch('indexedFieldTypeFilter', () => {
- if ($scope.indexedFieldTypeFilter !== undefined && $state.tab === 'indexedFields') {
- updateIndexedFieldsTable($scope, $state);
+ if ($scope.indexedFieldTypeFilter !== undefined && getCurrentTab() === TAB_INDEXED_FIELDS) {
+ updateIndexedFieldsTable($scope);
}
});
$scope.$watch('scriptedFieldLanguageFilter', () => {
- if ($scope.scriptedFieldLanguageFilter !== undefined && $state.tab === 'scriptedFields') {
- updateScriptedFieldsTable($scope, $state);
+ if (
+ $scope.scriptedFieldLanguageFilter !== undefined &&
+ getCurrentTab() === TAB_SCRIPTED_FIELDS
+ ) {
+ updateScriptedFieldsTable($scope);
}
});
$scope.$on('$destroy', () => {
destroyIndexedFieldsTable();
destroyScriptedFieldsTable();
+ destroySourceFiltersTable();
+ destroyState();
});
-
- updateScriptedFieldsTable($scope, $state);
- updateSourceFiltersTable($scope, $state);
});
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern_state_container.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern_state_container.ts
new file mode 100644
index 0000000000000..473417a7aabd6
--- /dev/null
+++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern_state_container.ts
@@ -0,0 +1,89 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { createHashHistory } from 'history';
+import {
+ createStateContainer,
+ syncState,
+ createKbnUrlStateStorage,
+} from '../../../../../../../../plugins/kibana_utils/public';
+
+interface IEditIndexPatternState {
+ tab: string; // TODO: type those 3 tabs with enum, when edit_index_pattern.js migrated to ts
+}
+
+/**
+ * Create state container with sync config for tab navigation specific for edit_index_pattern page
+ */
+export function createEditIndexPatternPageStateContainer({
+ defaultTab,
+ useHashedUrl,
+}: {
+ defaultTab: string;
+ useHashedUrl: boolean;
+}) {
+ // until angular is used as shell - use hash history
+ const history = createHashHistory();
+ // query param to store app state at
+ const stateStorageKey = '_a';
+ // default app state, when there is no initial state in the url
+ const defaultState = {
+ tab: defaultTab,
+ };
+ const kbnUrlStateStorage = createKbnUrlStateStorage({
+ useHash: useHashedUrl,
+ history,
+ });
+ // extract starting app state from URL and use it as starting app state in state container
+ const initialStateFromUrl = kbnUrlStateStorage.get(stateStorageKey);
+ const stateContainer = createStateContainer(
+ {
+ ...defaultState,
+ ...initialStateFromUrl,
+ },
+ {
+ setTab: (state: IEditIndexPatternState) => (tab: string) => ({ ...state, tab }),
+ },
+ {
+ tab: (state: IEditIndexPatternState) => () => state.tab,
+ }
+ );
+
+ const { start, stop } = syncState({
+ storageKey: stateStorageKey,
+ stateContainer: {
+ ...stateContainer,
+ // state syncing utility requires state containers to handle "null"
+ set: state => state && stateContainer.set(state),
+ },
+ stateStorage: kbnUrlStateStorage,
+ });
+
+ // makes sure initial url is the same as initial state (this is not really required)
+ kbnUrlStateStorage.set(stateStorageKey, stateContainer.getState(), { replace: true });
+
+ // expose api needed for Controller
+ return {
+ startSyncingState: start,
+ stopSyncingState: stop,
+ setCurrentTab: (newTab: string) => stateContainer.transitions.setTab(newTab),
+ getCurrentTab: () => stateContainer.selectors.tab(),
+ state$: stateContainer.state$,
+ };
+}
diff --git a/src/legacy/core_plugins/kibana/server/tutorials/activemq_metrics/index.js b/src/legacy/core_plugins/kibana/server/tutorials/activemq_metrics/index.js
index 683a58c85e552..7ed2d370debd1 100644
--- a/src/legacy/core_plugins/kibana/server/tutorials/activemq_metrics/index.js
+++ b/src/legacy/core_plugins/kibana/server/tutorials/activemq_metrics/index.js
@@ -48,7 +48,7 @@ export function activemqMetricsSpecProvider(context) {
isBeta: true,
artifacts: {
application: {
- label: i18n.translate('kbn.server.tutorials.corednsMetrics.artifacts.application.label', {
+ label: i18n.translate('kbn.server.tutorials.activemqMetrics.artifacts.application.label', {
defaultMessage: 'Discover',
}),
path: '/app/kibana#/discover',
diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/vega_parser.js b/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/vega_parser.js
deleted file mode 100644
index 50bcff2469710..0000000000000
--- a/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/vega_parser.js
+++ /dev/null
@@ -1,317 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import _ from 'lodash';
-import expect from '@kbn/expect';
-import { VegaParser } from '../vega_parser';
-import { bypassExternalUrlCheck } from '../../vega_view/vega_base_view';
-
-describe(`VegaParser._setDefaultValue`, () => {
- function test(spec, expected, ...params) {
- return () => {
- const vp = new VegaParser(spec);
- vp._setDefaultValue(...params);
- expect(vp.spec).to.eql(expected);
- expect(vp.warnings).to.have.length(0);
- };
- }
-
- it(`empty`, test({}, { config: { test: 42 } }, 42, 'config', 'test'));
- it(`exists`, test({ config: { test: 42 } }, { config: { test: 42 } }, 1, 'config', 'test'));
- it(`exists non-obj`, test({ config: false }, { config: false }, 42, 'config', 'test'));
-});
-
-describe(`VegaParser._setDefaultColors`, () => {
- function test(spec, isVegaLite, expected) {
- return () => {
- const vp = new VegaParser(spec);
- vp.isVegaLite = isVegaLite;
- vp._setDefaultColors();
- expect(vp.spec).to.eql(expected);
- expect(vp.warnings).to.have.length(0);
- };
- }
-
- it(
- `vegalite`,
- test({}, true, {
- config: {
- range: { category: { scheme: 'elastic' } },
- mark: { color: '#54B399' },
- },
- })
- );
-
- it(
- `vega`,
- test({}, false, {
- config: {
- range: { category: { scheme: 'elastic' } },
- arc: { fill: '#54B399' },
- area: { fill: '#54B399' },
- line: { stroke: '#54B399' },
- path: { stroke: '#54B399' },
- rect: { fill: '#54B399' },
- rule: { stroke: '#54B399' },
- shape: { stroke: '#54B399' },
- symbol: { fill: '#54B399' },
- trail: { fill: '#54B399' },
- },
- })
- );
-});
-
-describe('VegaParser._resolveEsQueries', () => {
- function test(spec, expected, warnCount) {
- return async () => {
- const vp = new VegaParser(spec, { search: async () => [[42]] }, 0, 0, {
- getFileLayers: async () => [{ name: 'file1', url: 'url1' }],
- getUrlForRegionLayer: async layer => {
- return layer.url;
- },
- });
- await vp._resolveDataUrls();
-
- expect(vp.spec).to.eql(expected);
- expect(vp.warnings).to.have.length(warnCount || 0);
- };
- }
-
- it('no data', test({}, {}));
- it('no data2', test({ a: 1 }, { a: 1 }));
- it('non-es data', test({ data: { a: 10 } }, { data: { a: 10 } }));
- it('es', test({ data: { url: { index: 'a' }, x: 1 } }, { data: { values: [42], x: 1 } }));
- it(
- 'es',
- test({ data: { url: { '%type%': 'elasticsearch', index: 'a' } } }, { data: { values: [42] } })
- );
- it(
- 'es arr',
- test(
- { arr: [{ data: { url: { index: 'a' }, x: 1 } }] },
- { arr: [{ data: { values: [42], x: 1 } }] }
- )
- );
- it(
- 'emsfile',
- test(
- { data: { url: { '%type%': 'emsfile', name: 'file1' } } },
- { data: { url: bypassExternalUrlCheck('url1') } }
- )
- );
-});
-
-describe('VegaParser._parseSchema', () => {
- function test(schema, isVegaLite, warningCount) {
- return () => {
- const vp = new VegaParser({ $schema: schema });
- expect(vp._parseSchema()).to.be(isVegaLite);
- expect(vp.spec).to.eql({ $schema: schema });
- expect(vp.warnings).to.have.length(warningCount);
- };
- }
-
- it('should warn on no vega version specified', () => {
- const vp = new VegaParser({});
- expect(vp._parseSchema()).to.be(false);
- expect(vp.spec).to.eql({ $schema: 'https://vega.github.io/schema/vega/v3.0.json' });
- expect(vp.warnings).to.have.length(1);
- });
-
- it(
- 'should not warn on current vega version',
- test('https://vega.github.io/schema/vega/v4.0.json', false, 0)
- );
- it(
- 'should not warn on older vega version',
- test('https://vega.github.io/schema/vega/v3.0.json', false, 0)
- );
- it(
- 'should warn on vega version too new to be supported',
- test('https://vega.github.io/schema/vega/v5.0.json', false, 1)
- );
-
- it(
- 'should not warn on current vega-lite version',
- test('https://vega.github.io/schema/vega-lite/v2.0.json', true, 0)
- );
- it(
- 'should warn on vega-lite version too new to be supported',
- test('https://vega.github.io/schema/vega-lite/v3.0.json', true, 1)
- );
-});
-
-describe('VegaParser._parseTooltips', () => {
- function test(tooltips, position, padding, centerOnMark) {
- return () => {
- const vp = new VegaParser(tooltips !== undefined ? { config: { kibana: { tooltips } } } : {});
- vp._config = vp._parseConfig();
- if (position === undefined) {
- // error
- expect(() => vp._parseTooltips()).to.throwError();
- } else if (position === false) {
- expect(vp._parseTooltips()).to.eql(false);
- } else {
- expect(vp._parseTooltips()).to.eql({ position, padding, centerOnMark });
- }
- };
- }
-
- it('undefined', test(undefined, 'top', 16, 50));
- it('{}', test({}, 'top', 16, 50));
- it('left', test({ position: 'left' }, 'left', 16, 50));
- it('padding', test({ position: 'bottom', padding: 60 }, 'bottom', 60, 50));
- it('padding2', test({ padding: 70 }, 'top', 70, 50));
- it('centerOnMark', test({}, 'top', 16, 50));
- it('centerOnMark=10', test({ centerOnMark: 10 }, 'top', 16, 10));
- it('centerOnMark=true', test({ centerOnMark: true }, 'top', 16, Number.MAX_VALUE));
- it('centerOnMark=false', test({ centerOnMark: false }, 'top', 16, -1));
-
- it('false', test(false, false));
-
- it('err1', test(true, undefined));
- it('err2', test({ position: 'foo' }, undefined));
- it('err3', test({ padding: 'foo' }, undefined));
- it('err4', test({ centerOnMark: {} }, undefined));
-});
-
-describe('VegaParser._parseMapConfig', () => {
- function test(config, expected, warnCount) {
- return () => {
- const vp = new VegaParser();
- vp._config = config;
- expect(vp._parseMapConfig()).to.eql(expected);
- expect(vp.warnings).to.have.length(warnCount);
- };
- }
-
- it(
- 'empty',
- test(
- {},
- {
- delayRepaint: true,
- latitude: 0,
- longitude: 0,
- mapStyle: 'default',
- zoomControl: true,
- scrollWheelZoom: false,
- },
- 0
- )
- );
-
- it(
- 'filled',
- test(
- {
- delayRepaint: true,
- latitude: 0,
- longitude: 0,
- mapStyle: 'default',
- zoomControl: true,
- scrollWheelZoom: false,
- maxBounds: [1, 2, 3, 4],
- },
- {
- delayRepaint: true,
- latitude: 0,
- longitude: 0,
- mapStyle: 'default',
- zoomControl: true,
- scrollWheelZoom: false,
- maxBounds: [1, 2, 3, 4],
- },
- 0
- )
- );
-
- it(
- 'warnings',
- test(
- {
- delayRepaint: true,
- latitude: 0,
- longitude: 0,
- zoom: 'abc', // ignored
- mapStyle: 'abc',
- zoomControl: 'abc',
- scrollWheelZoom: 'abc',
- maxBounds: [2, 3, 4],
- },
- {
- delayRepaint: true,
- latitude: 0,
- longitude: 0,
- mapStyle: 'default',
- zoomControl: true,
- scrollWheelZoom: false,
- },
- 5
- )
- );
-});
-
-describe('VegaParser._parseConfig', () => {
- function test(spec, expectedConfig, expectedSpec, warnCount) {
- return async () => {
- expectedSpec = expectedSpec || _.cloneDeep(spec);
- const vp = new VegaParser(spec);
- const config = await vp._parseConfig();
- expect(config).to.eql(expectedConfig);
- expect(vp.spec).to.eql(expectedSpec);
- expect(vp.warnings).to.have.length(warnCount || 0);
- };
- }
-
- it('no config', test({}, {}, {}));
- it('simple config', test({ config: { a: 1 } }, {}));
- it('kibana config', test({ config: { kibana: { a: 1 } } }, { a: 1 }, { config: {} }));
- it('_hostConfig', test({ _hostConfig: { a: 1 } }, { a: 1 }, {}, 1));
-});
-
-describe('VegaParser._calcSizing', () => {
- function test(spec, useResize, paddingWidth, paddingHeight, isVegaLite, expectedSpec, warnCount) {
- return async () => {
- expectedSpec = expectedSpec || _.cloneDeep(spec);
- const vp = new VegaParser(spec);
- vp.isVegaLite = !!isVegaLite;
- vp._calcSizing();
- expect(vp.useResize).to.eql(useResize);
- expect(vp.paddingWidth).to.eql(paddingWidth);
- expect(vp.paddingHeight).to.eql(paddingHeight);
- expect(vp.spec).to.eql(expectedSpec);
- expect(vp.warnings).to.have.length(warnCount || 0);
- };
- }
-
- it('no size', test({ autosize: {} }, false, 0, 0));
- it('fit', test({ autosize: 'fit' }, true, 0, 0));
- it('fit obj', test({ autosize: { type: 'fit' } }, true, 0, 0));
- it('padding const', test({ autosize: 'fit', padding: 10 }, true, 20, 20));
- it(
- 'padding obj',
- test({ autosize: 'fit', padding: { left: 5, bottom: 7, right: 6, top: 8 } }, true, 11, 15)
- );
- it('width height', test({ autosize: 'fit', width: 1, height: 2 }, true, 0, 0, false, false, 1));
- it(
- 'VL width height',
- test({ autosize: 'fit', width: 1, height: 2 }, true, 0, 0, true, { autosize: 'fit' }, 0)
- );
-});
diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/es_query_parser.js b/src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.test.js
similarity index 59%
rename from src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/es_query_parser.js
rename to src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.test.js
index a725bb9ed4cb5..691e5e8944241 100644
--- a/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/es_query_parser.js
+++ b/src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.test.js
@@ -17,11 +17,13 @@
* under the License.
*/
-import _ from 'lodash';
-import expect from '@kbn/expect';
-import sinon from 'sinon';
+import { cloneDeep } from 'lodash';
import moment from 'moment';
-import { EsQueryParser } from '../es_query_parser';
+import { EsQueryParser } from './es_query_parser';
+
+jest.mock('../helpers', () => ({
+ getEsShardTimeout: jest.fn(() => '10000'),
+}));
const second = 1000;
const minute = 60 * second;
@@ -39,41 +41,57 @@ function create(min, max, dashboardCtx) {
getTimeBounds: () => ({ min, max }),
},
() => {},
- _.cloneDeep(dashboardCtx),
+ cloneDeep(dashboardCtx),
() => (inst.$$$warnCount = (inst.$$$warnCount || 0) + 1)
);
return inst;
}
describe(`EsQueryParser time`, () => {
- it(`roundInterval(4s)`, () => expect(EsQueryParser._roundInterval(4 * second)).to.be(`1s`));
- it(`roundInterval(4hr)`, () => expect(EsQueryParser._roundInterval(4 * hour)).to.be(`3h`));
- it(`getTimeBound`, () => expect(create(1000, 2000)._getTimeBound({}, `min`)).to.be(1000));
- it(`getTimeBound(shift 2d)`, () =>
- expect(create(5, 2000)._getTimeBound({ shift: 2 }, `min`)).to.be(5 + 2 * day));
- it(`getTimeBound(shift -2hr)`, () =>
- expect(create(10 * day, 20 * day)._getTimeBound({ shift: -2, unit: `h` }, `min`)).to.be(
+ test(`roundInterval(4s)`, () => {
+ expect(EsQueryParser._roundInterval(4 * second)).toBe(`1s`);
+ });
+
+ test(`roundInterval(4hr)`, () => {
+ expect(EsQueryParser._roundInterval(4 * hour)).toBe(`3h`);
+ });
+
+ test(`getTimeBound`, () => {
+ expect(create(1000, 2000)._getTimeBound({}, `min`)).toBe(1000);
+ });
+
+ test(`getTimeBound(shift 2d)`, () => {
+ expect(create(5, 2000)._getTimeBound({ shift: 2 }, `min`)).toBe(5 + 2 * day);
+ });
+
+ test(`getTimeBound(shift -2hr)`, () => {
+ expect(create(10 * day, 20 * day)._getTimeBound({ shift: -2, unit: `h` }, `min`)).toBe(
10 * day - 2 * hour
- ));
- it(`createRangeFilter({})`, () => {
+ );
+ });
+
+ test(`createRangeFilter({})`, () => {
const obj = {};
- expect(create(1000, 2000)._createRangeFilter(obj))
- .to.eql({
- format: 'strict_date_optional_time',
- gte: moment(1000).toISOString(),
- lte: moment(2000).toISOString(),
- })
- .and.to.be(obj);
+ const result = create(1000, 2000)._createRangeFilter(obj);
+
+ expect(result).toEqual({
+ format: 'strict_date_optional_time',
+ gte: moment(1000).toISOString(),
+ lte: moment(2000).toISOString(),
+ });
+ expect(result).toBe(obj);
});
- it(`createRangeFilter(shift 1s)`, () => {
+
+ test(`createRangeFilter(shift 1s)`, () => {
const obj = { shift: 5, unit: 's' };
- expect(create(1000, 2000)._createRangeFilter(obj))
- .to.eql({
- format: 'strict_date_optional_time',
- gte: moment(6000).toISOString(),
- lte: moment(7000).toISOString(),
- })
- .and.to.be(obj);
+ const result = create(1000, 2000)._createRangeFilter(obj);
+
+ expect(result).toEqual({
+ format: 'strict_date_optional_time',
+ gte: moment(6000).toISOString(),
+ lte: moment(7000).toISOString(),
+ });
+ expect(result).toBe(obj);
});
});
@@ -82,79 +100,78 @@ describe('EsQueryParser.populateData', () => {
let parser;
beforeEach(() => {
- searchStub = sinon.stub();
+ searchStub = jest.fn(() => Promise.resolve([{}, {}]));
parser = new EsQueryParser({}, { search: searchStub }, undefined, undefined);
-
- searchStub.returns(Promise.resolve([{}, {}]));
});
- it('should set the timeout for each request', async () => {
+
+ test('should set the timeout for each request', async () => {
await parser.populateData([
{ url: { body: {} }, dataObject: {} },
{ url: { body: {} }, dataObject: {} },
]);
- expect(searchStub.firstCall.args[0][0].body.timeout).to.be.defined;
+ expect(searchStub.mock.calls[0][0][0].body.timeout).toBe.defined;
});
- it('should remove possible timeout parameters on a request', async () => {
+ test('should remove possible timeout parameters on a request', async () => {
await parser.populateData([
{ url: { timeout: '500h', body: { timeout: '500h' } }, dataObject: {} },
]);
- expect(searchStub.firstCall.args[0][0].body.timeout).to.be.defined;
- expect(searchStub.firstCall.args[0][0].timeout).to.be(undefined);
+ expect(searchStub.mock.calls[0][0][0].body.timeout).toBe.defined;
+ expect(searchStub.mock.calls[0][0][0].timeout).toBe(undefined);
});
});
describe(`EsQueryParser.injectQueryContextVars`, () => {
- function test(obj, expected, ctx) {
+ function check(obj, expected, ctx) {
return () => {
create(rangeStart, rangeEnd, ctx)._injectContextVars(obj, true);
- expect(obj).to.eql(expected);
+ expect(obj).toEqual(expected);
};
}
- it(`empty`, test({}, {}));
- it(`simple`, () => {
+ test(`empty`, check({}, {}));
+ test(`simple`, () => {
const obj = { a: { c: 10 }, b: [{ d: 2 }, 4, 5], c: [], d: {} };
- test(obj, _.cloneDeep(obj));
+ check(obj, cloneDeep(obj));
});
- it(`must clause empty`, test({ arr: ['%dashboard_context-must_clause%'] }, { arr: [] }, {}));
- it(
+ test(`must clause empty`, check({ arr: ['%dashboard_context-must_clause%'] }, { arr: [] }, {}));
+ test(
`must clause arr`,
- test({ arr: ['%dashboard_context-must_clause%'] }, { arr: [...ctxArr.bool.must] }, ctxArr)
+ check({ arr: ['%dashboard_context-must_clause%'] }, { arr: [...ctxArr.bool.must] }, ctxArr)
);
- it(
+ test(
`must clause obj`,
- test({ arr: ['%dashboard_context-must_clause%'] }, { arr: [ctxObj.bool.must] }, ctxObj)
+ check({ arr: ['%dashboard_context-must_clause%'] }, { arr: [ctxObj.bool.must] }, ctxObj)
);
- it(
+ test(
`mixed clause arr`,
- test(
+ check(
{ arr: [1, '%dashboard_context-must_clause%', 2, '%dashboard_context-must_not_clause%'] },
{ arr: [1, ...ctxArr.bool.must, 2, ...ctxArr.bool.must_not] },
ctxArr
)
);
- it(
+ test(
`mixed clause obj`,
- test(
+ check(
{ arr: ['%dashboard_context-must_clause%', 1, '%dashboard_context-must_not_clause%', 2] },
{ arr: [ctxObj.bool.must, 1, ctxObj.bool.must_not, 2] },
ctxObj
)
);
- it(
+ test(
`%autointerval% = true`,
- test({ interval: { '%autointerval%': true } }, { interval: `1h` }, ctxObj)
+ check({ interval: { '%autointerval%': true } }, { interval: `1h` }, ctxObj)
);
- it(
+ test(
`%autointerval% = 10`,
- test({ interval: { '%autointerval%': 10 } }, { interval: `3h` }, ctxObj)
+ check({ interval: { '%autointerval%': 10 } }, { interval: `3h` }, ctxObj)
);
- it(`%timefilter% = min`, test({ a: { '%timefilter%': 'min' } }, { a: rangeStart }));
- it(`%timefilter% = max`, test({ a: { '%timefilter%': 'max' } }, { a: rangeEnd }));
- it(
+ test(`%timefilter% = min`, check({ a: { '%timefilter%': 'min' } }, { a: rangeStart }));
+ test(`%timefilter% = max`, check({ a: { '%timefilter%': 'max' } }, { a: rangeEnd }));
+ test(
`%timefilter% = true`,
- test(
+ check(
{ a: { '%timefilter%': true } },
{
a: {
@@ -168,24 +185,24 @@ describe(`EsQueryParser.injectQueryContextVars`, () => {
});
describe(`EsQueryParser.parseEsRequest`, () => {
- function test(req, ctx, expected) {
+ function check(req, ctx, expected) {
return () => {
create(rangeStart, rangeEnd, ctx).parseUrl({}, req);
- expect(req).to.eql(expected);
+ expect(req).toEqual(expected);
};
}
- it(
+ test(
`%context_query%=true`,
- test({ index: '_all', '%context_query%': true }, ctxArr, {
+ check({ index: '_all', '%context_query%': true }, ctxArr, {
index: '_all',
body: { query: ctxArr },
})
);
- it(
+ test(
`%context%=true`,
- test({ index: '_all', '%context%': true }, ctxArr, { index: '_all', body: { query: ctxArr } })
+ check({ index: '_all', '%context%': true }, ctxArr, { index: '_all', body: { query: ctxArr } })
);
const expectedForCtxAndTimefield = {
@@ -211,23 +228,23 @@ describe(`EsQueryParser.parseEsRequest`, () => {
},
};
- it(
+ test(
`%context_query%='abc'`,
- test({ index: '_all', '%context_query%': 'abc' }, ctxArr, expectedForCtxAndTimefield)
+ check({ index: '_all', '%context_query%': 'abc' }, ctxArr, expectedForCtxAndTimefield)
);
- it(
+ test(
`%context%=true, %timefield%='abc'`,
- test(
+ check(
{ index: '_all', '%context%': true, '%timefield%': 'abc' },
ctxArr,
expectedForCtxAndTimefield
)
);
- it(
+ test(
`%timefield%='abc'`,
- test({ index: '_all', '%timefield%': 'abc' }, ctxArr, {
+ check({ index: '_all', '%timefield%': 'abc' }, ctxArr, {
index: '_all',
body: {
query: {
@@ -243,11 +260,11 @@ describe(`EsQueryParser.parseEsRequest`, () => {
})
);
- it(`no esRequest`, test({ index: '_all' }, ctxArr, { index: '_all', body: {} }));
+ test(`no esRequest`, check({ index: '_all' }, ctxArr, { index: '_all', body: {} }));
- it(
+ test(
`esRequest`,
- test({ index: '_all', body: { query: 2 } }, ctxArr, {
+ check({ index: '_all', body: { query: 2 } }, ctxArr, {
index: '_all',
body: { query: 2 },
})
diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/search_cache.js b/src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.test.js
similarity index 75%
rename from src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/search_cache.js
rename to src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.test.js
index 2b28d2cadfa3f..0ec018f46c02b 100644
--- a/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/search_cache.js
+++ b/src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.test.js
@@ -17,8 +17,7 @@
* under the License.
*/
-import expect from '@kbn/expect';
-import { SearchCache } from '../search_cache';
+import { SearchCache } from './search_cache';
describe(`SearchCache`, () => {
class FauxEs {
@@ -45,27 +44,27 @@ describe(`SearchCache`, () => {
// empty request
let res = await sc.search([]);
- expect(res).to.eql([]);
- expect(sc._es.searches).to.eql([]);
+ expect(res).toEqual([]);
+ expect(sc._es.searches).toEqual([]);
// single request
res = await sc.search([request1]);
- expect(res).to.eql([expected1]);
- expect(sc._es.searches).to.eql([request1]);
+ expect(res).toEqual([expected1]);
+ expect(sc._es.searches).toEqual([request1]);
// repeat the same search, use array notation
res = await sc.search([request1]);
- expect(res).to.eql([expected1]);
- expect(sc._es.searches).to.eql([request1]); // no new entries
+ expect(res).toEqual([expected1]);
+ expect(sc._es.searches).toEqual([request1]); // no new entries
// new single search
res = await sc.search([request2]);
- expect(res).to.eql([expected2]);
- expect(sc._es.searches).to.eql([request1, request2]);
+ expect(res).toEqual([expected2]);
+ expect(sc._es.searches).toEqual([request1, request2]);
// multiple search, some new, some old
res = await sc.search([request1, request3, request2]);
- expect(res).to.eql([expected1, expected3, expected2]);
- expect(sc._es.searches).to.eql([request1, request2, request3]);
+ expect(res).toEqual([expected1, expected3, expected2]);
+ expect(sc._es.searches).toEqual([request1, request2, request3]);
});
});
diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/time_cache.js b/src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.test.js
similarity index 72%
rename from src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/time_cache.js
rename to src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.test.js
index 21e9256295382..b76709ea2c934 100644
--- a/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/time_cache.js
+++ b/src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.test.js
@@ -17,8 +17,7 @@
* under the License.
*/
-import expect from '@kbn/expect';
-import { TimeCache } from '../time_cache';
+import { TimeCache } from './time_cache';
describe(`TimeCache`, () => {
class FauxTimefilter {
@@ -70,29 +69,29 @@ describe(`TimeCache`, () => {
let filterAccess = 0;
// first call - gets bounds
- expect(tc.getTimeBounds()).to.eql({ min: 10000, max: 20000 });
- expect(time._accessCount).to.be(++timeAccess);
- expect(timefilter._accessCount).to.be(++filterAccess);
+ expect(tc.getTimeBounds()).toEqual({ min: 10000, max: 20000 });
+ expect(time._accessCount).toBe(++timeAccess);
+ expect(timefilter._accessCount).toBe(++filterAccess);
// short diff, same result
time.increment(10);
timefilter.setTime(10010, 20010);
- expect(tc.getTimeBounds()).to.eql({ min: 10000, max: 20000 });
- expect(time._accessCount).to.be(++timeAccess);
- expect(timefilter._accessCount).to.be(filterAccess);
+ expect(tc.getTimeBounds()).toEqual({ min: 10000, max: 20000 });
+ expect(time._accessCount).toBe(++timeAccess);
+ expect(timefilter._accessCount).toBe(filterAccess);
// longer diff, gets bounds but returns original
time.increment(200);
timefilter.setTime(10210, 20210);
- expect(tc.getTimeBounds()).to.eql({ min: 10000, max: 20000 });
- expect(time._accessCount).to.be(++timeAccess);
- expect(timefilter._accessCount).to.be(++filterAccess);
+ expect(tc.getTimeBounds()).toEqual({ min: 10000, max: 20000 });
+ expect(time._accessCount).toBe(++timeAccess);
+ expect(timefilter._accessCount).toBe(++filterAccess);
// long diff, new result
time.increment(10000);
timefilter.setTime(20220, 30220);
- expect(tc.getTimeBounds()).to.eql({ min: 20220, max: 30220 });
- expect(time._accessCount).to.be(++timeAccess);
- expect(timefilter._accessCount).to.be(++filterAccess);
+ expect(tc.getTimeBounds()).toEqual({ min: 20220, max: 30220 });
+ expect(time._accessCount).toBe(++timeAccess);
+ expect(timefilter._accessCount).toBe(++filterAccess);
});
});
diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.test.js b/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.test.js
new file mode 100644
index 0000000000000..1bc8b1f90daab
--- /dev/null
+++ b/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.test.js
@@ -0,0 +1,327 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { cloneDeep } from 'lodash';
+import { VegaParser } from './vega_parser';
+import { bypassExternalUrlCheck } from '../vega_view/vega_base_view';
+
+describe(`VegaParser._setDefaultValue`, () => {
+ function check(spec, expected, ...params) {
+ return () => {
+ const vp = new VegaParser(spec);
+ vp._setDefaultValue(...params);
+ expect(vp.spec).toEqual(expected);
+ expect(vp.warnings).toHaveLength(0);
+ };
+ }
+
+ test(`empty`, check({}, { config: { test: 42 } }, 42, 'config', 'test'));
+ test(`exists`, check({ config: { test: 42 } }, { config: { test: 42 } }, 1, 'config', 'test'));
+ test(`exists non-obj`, check({ config: false }, { config: false }, 42, 'config', 'test'));
+});
+
+describe(`VegaParser._setDefaultColors`, () => {
+ function check(spec, isVegaLite, expected) {
+ return () => {
+ const vp = new VegaParser(spec);
+ vp.isVegaLite = isVegaLite;
+ vp._setDefaultColors();
+ expect(vp.spec).toEqual(expected);
+ expect(vp.warnings).toHaveLength(0);
+ };
+ }
+
+ test(
+ `vegalite`,
+ check({}, true, {
+ config: {
+ range: { category: { scheme: 'elastic' } },
+ mark: { color: '#54B399' },
+ },
+ })
+ );
+
+ test(
+ `vega`,
+ check({}, false, {
+ config: {
+ range: { category: { scheme: 'elastic' } },
+ arc: { fill: '#54B399' },
+ area: { fill: '#54B399' },
+ line: { stroke: '#54B399' },
+ path: { stroke: '#54B399' },
+ rect: { fill: '#54B399' },
+ rule: { stroke: '#54B399' },
+ shape: { stroke: '#54B399' },
+ symbol: { fill: '#54B399' },
+ trail: { fill: '#54B399' },
+ },
+ })
+ );
+});
+
+describe('VegaParser._resolveEsQueries', () => {
+ function check(spec, expected, warnCount) {
+ return async () => {
+ const vp = new VegaParser(spec, { search: async () => [[42]] }, 0, 0, {
+ getFileLayers: async () => [{ name: 'file1', url: 'url1' }],
+ getUrlForRegionLayer: async layer => {
+ return layer.url;
+ },
+ });
+ await vp._resolveDataUrls();
+
+ expect(vp.spec).toEqual(expected);
+ expect(vp.warnings).toHaveLength(warnCount || 0);
+ };
+ }
+
+ test('no data', check({}, {}));
+ test('no data2', check({ a: 1 }, { a: 1 }));
+ test('non-es data', check({ data: { a: 10 } }, { data: { a: 10 } }));
+ test('es', check({ data: { url: { index: 'a' }, x: 1 } }, { data: { values: [42], x: 1 } }));
+ test(
+ 'es 2',
+ check({ data: { url: { '%type%': 'elasticsearch', index: 'a' } } }, { data: { values: [42] } })
+ );
+ test(
+ 'es arr',
+ check(
+ { arr: [{ data: { url: { index: 'a' }, x: 1 } }] },
+ { arr: [{ data: { values: [42], x: 1 } }] }
+ )
+ );
+ test(
+ 'emsfile',
+ check(
+ { data: { url: { '%type%': 'emsfile', name: 'file1' } } },
+ { data: { url: bypassExternalUrlCheck('url1') } }
+ )
+ );
+});
+
+describe('VegaParser._parseSchema', () => {
+ function check(schema, isVegaLite, warningCount) {
+ return () => {
+ const vp = new VegaParser({ $schema: schema });
+ expect(vp._parseSchema()).toBe(isVegaLite);
+ expect(vp.spec).toEqual({ $schema: schema });
+ expect(vp.warnings).toHaveLength(warningCount);
+ };
+ }
+
+ test('should warn on no vega version specified', () => {
+ const vp = new VegaParser({});
+ expect(vp._parseSchema()).toBe(false);
+ expect(vp.spec).toEqual({ $schema: 'https://vega.github.io/schema/vega/v3.0.json' });
+ expect(vp.warnings).toHaveLength(1);
+ });
+
+ test(
+ 'should not warn on current vega version',
+ check('https://vega.github.io/schema/vega/v4.0.json', false, 0)
+ );
+ test(
+ 'should not warn on older vega version',
+ check('https://vega.github.io/schema/vega/v3.0.json', false, 0)
+ );
+ test(
+ 'should warn on vega version too new to be supported',
+ check('https://vega.github.io/schema/vega/v5.0.json', false, 1)
+ );
+
+ test(
+ 'should not warn on current vega-lite version',
+ check('https://vega.github.io/schema/vega-lite/v2.0.json', true, 0)
+ );
+ test(
+ 'should warn on vega-lite version too new to be supported',
+ check('https://vega.github.io/schema/vega-lite/v3.0.json', true, 1)
+ );
+});
+
+describe('VegaParser._parseTooltips', () => {
+ function check(tooltips, position, padding, centerOnMark) {
+ return () => {
+ const vp = new VegaParser(tooltips !== undefined ? { config: { kibana: { tooltips } } } : {});
+ vp._config = vp._parseConfig();
+ if (position === undefined) {
+ // error
+ expect(() => vp._parseTooltips()).toThrow();
+ } else if (position === false) {
+ expect(vp._parseTooltips()).toEqual(false);
+ } else {
+ expect(vp._parseTooltips()).toEqual({ position, padding, centerOnMark });
+ }
+ };
+ }
+
+ test('undefined', check(undefined, 'top', 16, 50));
+ test('{}', check({}, 'top', 16, 50));
+ test('left', check({ position: 'left' }, 'left', 16, 50));
+ test('padding', check({ position: 'bottom', padding: 60 }, 'bottom', 60, 50));
+ test('padding2', check({ padding: 70 }, 'top', 70, 50));
+ test('centerOnMark', check({}, 'top', 16, 50));
+ test('centerOnMark=10', check({ centerOnMark: 10 }, 'top', 16, 10));
+ test('centerOnMark=true', check({ centerOnMark: true }, 'top', 16, Number.MAX_VALUE));
+ test('centerOnMark=false', check({ centerOnMark: false }, 'top', 16, -1));
+
+ test('false', check(false, false));
+
+ test('err1', check(true, undefined));
+ test('err2', check({ position: 'foo' }, undefined));
+ test('err3', check({ padding: 'foo' }, undefined));
+ test('err4', check({ centerOnMark: {} }, undefined));
+});
+
+describe('VegaParser._parseMapConfig', () => {
+ function check(config, expected, warnCount) {
+ return () => {
+ const vp = new VegaParser();
+ vp._config = config;
+ expect(vp._parseMapConfig()).toEqual(expected);
+ expect(vp.warnings).toHaveLength(warnCount);
+ };
+ }
+
+ test(
+ 'empty',
+ check(
+ {},
+ {
+ delayRepaint: true,
+ latitude: 0,
+ longitude: 0,
+ mapStyle: 'default',
+ zoomControl: true,
+ scrollWheelZoom: false,
+ },
+ 0
+ )
+ );
+
+ test(
+ 'filled',
+ check(
+ {
+ delayRepaint: true,
+ latitude: 0,
+ longitude: 0,
+ mapStyle: 'default',
+ zoomControl: true,
+ scrollWheelZoom: false,
+ maxBounds: [1, 2, 3, 4],
+ },
+ {
+ delayRepaint: true,
+ latitude: 0,
+ longitude: 0,
+ mapStyle: 'default',
+ zoomControl: true,
+ scrollWheelZoom: false,
+ maxBounds: [1, 2, 3, 4],
+ },
+ 0
+ )
+ );
+
+ test(
+ 'warnings',
+ check(
+ {
+ delayRepaint: true,
+ latitude: 0,
+ longitude: 0,
+ zoom: 'abc', // ignored
+ mapStyle: 'abc',
+ zoomControl: 'abc',
+ scrollWheelZoom: 'abc',
+ maxBounds: [2, 3, 4],
+ },
+ {
+ delayRepaint: true,
+ latitude: 0,
+ longitude: 0,
+ mapStyle: 'default',
+ zoomControl: true,
+ scrollWheelZoom: false,
+ },
+ 5
+ )
+ );
+});
+
+describe('VegaParser._parseConfig', () => {
+ function check(spec, expectedConfig, expectedSpec, warnCount) {
+ return async () => {
+ expectedSpec = expectedSpec || cloneDeep(spec);
+ const vp = new VegaParser(spec);
+ const config = await vp._parseConfig();
+ expect(config).toEqual(expectedConfig);
+ expect(vp.spec).toEqual(expectedSpec);
+ expect(vp.warnings).toHaveLength(warnCount || 0);
+ };
+ }
+
+ test('no config', check({}, {}, {}));
+ test('simple config', check({ config: { a: 1 } }, {}));
+ test('kibana config', check({ config: { kibana: { a: 1 } } }, { a: 1 }, { config: {} }));
+ test('_hostConfig', check({ _hostConfig: { a: 1 } }, { a: 1 }, {}, 1));
+});
+
+describe('VegaParser._calcSizing', () => {
+ function check(
+ spec,
+ useResize,
+ paddingWidth,
+ paddingHeight,
+ isVegaLite,
+ expectedSpec,
+ warnCount
+ ) {
+ return async () => {
+ expectedSpec = expectedSpec || cloneDeep(spec);
+ const vp = new VegaParser(spec);
+ vp.isVegaLite = !!isVegaLite;
+ vp._calcSizing();
+ expect(vp.useResize).toEqual(useResize);
+ expect(vp.paddingWidth).toEqual(paddingWidth);
+ expect(vp.paddingHeight).toEqual(paddingHeight);
+ expect(vp.spec).toEqual(expectedSpec);
+ expect(vp.warnings).toHaveLength(warnCount || 0);
+ };
+ }
+
+ test('no size', check({ autosize: {} }, false, 0, 0));
+ test('fit', check({ autosize: 'fit' }, true, 0, 0));
+ test('fit obj', check({ autosize: { type: 'fit' } }, true, 0, 0));
+ test('padding const', check({ autosize: 'fit', padding: 10 }, true, 20, 20));
+ test(
+ 'padding obj',
+ check({ autosize: 'fit', padding: { left: 5, bottom: 7, right: 6, top: 8 } }, true, 11, 15)
+ );
+ test(
+ 'width height',
+ check({ autosize: 'fit', width: 1, height: 2 }, true, 0, 0, false, false, 1)
+ );
+ test(
+ 'VL width height',
+ check({ autosize: 'fit', width: 1, height: 2 }, true, 0, 0, true, { autosize: 'fit' }, 0)
+ );
+});
diff --git a/src/legacy/ui/public/agg_types/param_types/field.ts b/src/legacy/ui/public/agg_types/param_types/field.ts
index 0ca60267becec..a0fa6ad6e3189 100644
--- a/src/legacy/ui/public/agg_types/param_types/field.ts
+++ b/src/legacy/ui/public/agg_types/param_types/field.ts
@@ -115,7 +115,10 @@ export class FieldParamType extends BaseParamType {
const filteredFields = fields.filter((field: Field) => {
const { onlyAggregatable, scriptable, filterFieldTypes } = this;
- if ((onlyAggregatable && !field.aggregatable) || (!scriptable && field.scripted)) {
+ if (
+ (onlyAggregatable && (!field.aggregatable || field.subType?.nested)) ||
+ (!scriptable && field.scripted)
+ ) {
return false;
}
diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.test.js b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.test.js
index 3ec903d5b18e4..8ddd18c2c67f4 100644
--- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.test.js
+++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.test.js
@@ -37,7 +37,7 @@ describe('index_patterns/field_capabilities/field_caps_response', () => {
describe('conflicts', () => {
it('returns a field for each in response, no filtering', () => {
const fields = readFieldCapsResponse(esResponse);
- expect(fields).toHaveLength(25);
+ expect(fields).toHaveLength(24);
});
it(
@@ -68,8 +68,8 @@ describe('index_patterns/field_capabilities/field_caps_response', () => {
sandbox.spy(shouldReadFieldFromDocValuesNS, 'shouldReadFieldFromDocValues');
const fields = readFieldCapsResponse(esResponse);
const conflictCount = fields.filter(f => f.type === 'conflict').length;
- // +1 is for the object field which is filtered out of the final return value from readFieldCapsResponse
- sinon.assert.callCount(shouldReadFieldFromDocValues, fields.length - conflictCount + 1);
+ // +2 is for the object and nested fields which get filtered out of the final return value from readFieldCapsResponse
+ sinon.assert.callCount(shouldReadFieldFromDocValues, fields.length - conflictCount + 2);
});
it('converts es types to kibana types', () => {
@@ -143,13 +143,6 @@ describe('index_patterns/field_capabilities/field_caps_response', () => {
expect(child).toHaveProperty('subType', { nested: { path: 'nested_object_parent' } });
});
- it('returns nested sub-fields as non-aggregatable', () => {
- const fields = readFieldCapsResponse(esResponse);
- // Normally a keyword field would be aggregatable, but the fact that it is nested overrides that
- const child = fields.find(f => f.name === 'nested_object_parent.child.keyword');
- expect(child).toHaveProperty('aggregatable', false);
- });
-
it('handles fields that are both nested and multi', () => {
const fields = readFieldCapsResponse(esResponse);
const child = fields.find(f => f.name === 'nested_object_parent.child.keyword');
@@ -159,12 +152,10 @@ describe('index_patterns/field_capabilities/field_caps_response', () => {
});
});
- it('returns the nested parent as not searchable or aggregatable', () => {
+ it('does not include the field actually mapped as nested itself', () => {
const fields = readFieldCapsResponse(esResponse);
const child = fields.find(f => f.name === 'nested_object_parent');
- expect(child.type).toBe('nested');
- expect(child.aggregatable).toBe(false);
- expect(child.searchable).toBe(false);
+ expect(child).toBeUndefined();
});
it('should not confuse object children for multi or nested field children', () => {
diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts
index 0c8c2ce48fa84..06eb30db0b24b 100644
--- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts
+++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts
@@ -182,19 +182,11 @@ export function readFieldCapsResponse(fieldCapsResponse: FieldCapsResponse): Fie
if (Object.keys(subType).length > 0) {
field.subType = subType;
-
- // We don't support aggregating on nested fields, trying to do so in the UI will return
- // blank results. For now we will stop showing nested fields as an option for aggregation.
- // Once we add support for nested fields this condition should be removed and old index
- // patterns should be migrated.
- if (field.subType.nested) {
- field.aggregatable = false;
- }
}
}
});
return kibanaFormattedCaps.filter(field => {
- return !['object'].includes(field.type);
+ return !['object', 'nested'].includes(field.type);
});
}
diff --git a/src/plugins/kibana_react/public/field_icon/field_icon.tsx b/src/plugins/kibana_react/public/field_icon/field_icon.tsx
index 7c44fe89d0e7f..2e199a7471a64 100644
--- a/src/plugins/kibana_react/public/field_icon/field_icon.tsx
+++ b/src/plugins/kibana_react/public/field_icon/field_icon.tsx
@@ -36,8 +36,8 @@ interface FieldIconProps {
| 'number'
| '_source'
| 'string'
- | 'nested'
- | string;
+ | string
+ | 'nested';
label?: string;
size?: IconSize;
useColor?: boolean;
diff --git a/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js b/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js
index 555056173ec62..c4c71abdae125 100644
--- a/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js
+++ b/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js
@@ -72,15 +72,7 @@ export default function({ getService }) {
readFromDocValues: true,
},
{
- aggregatable: false,
- esTypes: ['nested'],
- name: 'nestedField',
- readFromDocValues: false,
- searchable: false,
- type: 'nested',
- },
- {
- aggregatable: false,
+ aggregatable: true,
esTypes: ['keyword'],
name: 'nestedField.child',
readFromDocValues: true,
@@ -162,15 +154,7 @@ export default function({ getService }) {
readFromDocValues: true,
},
{
- aggregatable: false,
- esTypes: ['nested'],
- name: 'nestedField',
- readFromDocValues: false,
- searchable: false,
- type: 'nested',
- },
- {
- aggregatable: false,
+ aggregatable: true,
esTypes: ['keyword'],
name: 'nestedField.child',
readFromDocValues: true,
diff --git a/test/plugin_functional/plugins/core_app_status/public/application.tsx b/test/plugin_functional/plugins/core_app_status/public/application.tsx
index 323774392a6d7..b9ebd8d3692f1 100644
--- a/test/plugin_functional/plugins/core_app_status/public/application.tsx
+++ b/test/plugin_functional/plugins/core_app_status/public/application.tsx
@@ -31,15 +31,15 @@ import {
EuiTitle,
} from '@elastic/eui';
-import { AppMountContext, AppMountParameters } from 'kibana/public';
+import { AppMountParameters } from 'kibana/public';
-const AppStatusApp = () => (
+const AppStatusApp = ({ appId }: { appId: string }) => (
- Welcome to App Status Test App!
+ Welcome to {appId} Test App!
@@ -47,18 +47,18 @@ const AppStatusApp = () => (
- App Status Test App home page section title
+ {appId} Test App home page section title
- App Status Test App content
+ {appId} Test App content
);
-export const renderApp = (context: AppMountContext, { element }: AppMountParameters) => {
- render( , element);
+export const renderApp = (appId: string, { element }: AppMountParameters) => {
+ render( , element);
return () => unmountComponentAtNode(element);
};
diff --git a/test/plugin_functional/plugins/core_app_status/public/index.ts b/test/plugin_functional/plugins/core_app_status/public/index.ts
index e0ad7c25a54b8..f52b7ff5fea44 100644
--- a/test/plugin_functional/plugins/core_app_status/public/index.ts
+++ b/test/plugin_functional/plugins/core_app_status/public/index.ts
@@ -18,7 +18,7 @@
*/
import { PluginInitializer } from 'kibana/public';
-import { CoreAppStatusPlugin, CoreAppStatusPluginSetup, CoreAppStatusPluginStart } from './plugin';
+import { CoreAppStatusPlugin, CoreAppStatusPluginStart } from './plugin';
-export const plugin: PluginInitializer = () =>
+export const plugin: PluginInitializer<{}, CoreAppStatusPluginStart> = () =>
new CoreAppStatusPlugin();
diff --git a/test/plugin_functional/plugins/core_app_status/public/plugin.tsx b/test/plugin_functional/plugins/core_app_status/public/plugin.tsx
index 85caaaf5f9090..af23bfbe1f8f5 100644
--- a/test/plugin_functional/plugins/core_app_status/public/plugin.tsx
+++ b/test/plugin_functional/plugins/core_app_status/public/plugin.tsx
@@ -17,22 +17,38 @@
* under the License.
*/
-import { Plugin, CoreSetup, AppUpdater, AppUpdatableFields, CoreStart } from 'kibana/public';
import { BehaviorSubject } from 'rxjs';
+import {
+ Plugin,
+ CoreSetup,
+ AppUpdater,
+ AppUpdatableFields,
+ CoreStart,
+ AppMountParameters,
+} from 'kibana/public';
+import './types';
-export class CoreAppStatusPlugin
- implements Plugin {
+export class CoreAppStatusPlugin implements Plugin<{}, CoreAppStatusPluginStart> {
private appUpdater = new BehaviorSubject(() => ({}));
public setup(core: CoreSetup, deps: {}) {
+ core.application.register({
+ id: 'app_status_start',
+ title: 'App Status Start Page',
+ async mount(params: AppMountParameters) {
+ const { renderApp } = await import('./application');
+ return renderApp('app_status_start', params);
+ },
+ });
+
core.application.register({
id: 'app_status',
title: 'App Status',
euiIconType: 'snowflake',
updater$: this.appUpdater,
- async mount(context, params) {
+ async mount(params: AppMountParameters) {
const { renderApp } = await import('./application');
- return renderApp(context, params);
+ return renderApp('app_status', params);
},
});
@@ -40,7 +56,7 @@ export class CoreAppStatusPlugin
}
public start(core: CoreStart) {
- return {
+ const startContract = {
setAppStatus: (status: Partial) => {
this.appUpdater.next(() => status);
},
@@ -48,9 +64,10 @@ export class CoreAppStatusPlugin
return core.application.navigateToApp(appId);
},
};
+ window.__coreAppStatus = startContract;
+ return startContract;
}
public stop() {}
}
-export type CoreAppStatusPluginSetup = ReturnType;
export type CoreAppStatusPluginStart = ReturnType;
diff --git a/test/plugin_functional/plugins/core_app_status/public/types.ts b/test/plugin_functional/plugins/core_app_status/public/types.ts
new file mode 100644
index 0000000000000..7c708e6c26d91
--- /dev/null
+++ b/test/plugin_functional/plugins/core_app_status/public/types.ts
@@ -0,0 +1,26 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { CoreAppStatusPluginStart } from './plugin';
+
+declare global {
+ interface Window {
+ __coreAppStatus: CoreAppStatusPluginStart;
+ }
+}
diff --git a/test/plugin_functional/test_suites/core_plugins/application_status.ts b/test/plugin_functional/test_suites/core_plugins/application_status.ts
index 703ae30533bae..b6d13a5604011 100644
--- a/test/plugin_functional/test_suites/core_plugins/application_status.ts
+++ b/test/plugin_functional/test_suites/core_plugins/application_status.ts
@@ -24,50 +24,32 @@ import {
AppUpdatableFields,
} from '../../../../src/core/public/application/types';
import { PluginFunctionalProviderContext } from '../../services';
-import { CoreAppStatusPluginStart } from '../../plugins/core_app_status/public/plugin';
-import '../../plugins/core_provider_plugin/types';
+import '../../plugins/core_app_status/public/types';
// eslint-disable-next-line import/no-default-export
export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) {
const PageObjects = getPageObjects(['common']);
const browser = getService('browser');
const appsMenu = getService('appsMenu');
+ const testSubjects = getService('testSubjects');
const setAppStatus = async (s: Partial) => {
- await browser.executeAsync(async (status: Partial, cb: Function) => {
- const plugin = window.__coreProvider.start.plugins
- .core_app_status as CoreAppStatusPluginStart;
- plugin.setAppStatus(status);
+ return browser.executeAsync(async (status: Partial, cb: Function) => {
+ window.__coreAppStatus.setAppStatus(status);
cb();
}, s);
};
- const navigateToApp = async (i: string): Promise<{ error?: string }> => {
+ const navigateToApp = async (i: string) => {
return (await browser.executeAsync(async (appId, cb: Function) => {
- // navigating in legacy mode performs a page refresh
- // and webdriver seems to re-execute the script after the reload
- // as it considers it didn't end on the previous session.
- // however when testing navigation to NP app, __coreProvider is not accessible
- // so we need to check for existence.
- if (!window.__coreProvider) {
- cb({});
- }
- const plugin = window.__coreProvider.start.plugins
- .core_app_status as CoreAppStatusPluginStart;
- try {
- await plugin.navigateToApp(appId);
- cb({});
- } catch (e) {
- cb({
- error: e.message,
- });
- }
+ await window.__coreAppStatus.navigateToApp(appId);
+ cb();
}, i)) as any;
};
describe('application status management', () => {
beforeEach(async () => {
- await PageObjects.common.navigateToApp('settings');
+ await PageObjects.common.navigateToApp('app_status_start');
});
it('can change the navLink status at runtime', async () => {
@@ -98,10 +80,10 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider
status: AppStatus.inaccessible,
});
- const result = await navigateToApp('app_status');
- expect(result.error).to.contain(
- 'Trying to navigate to an inaccessible application: app_status'
- );
+ await navigateToApp('app_status');
+
+ expect(await testSubjects.exists('appNotFoundPageContent')).to.eql(true);
+ expect(await testSubjects.exists('appStatusApp')).to.eql(false);
});
it('allows to navigate to an accessible app', async () => {
@@ -109,8 +91,35 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider
status: AppStatus.accessible,
});
- const result = await navigateToApp('app_status');
- expect(result.error).to.eql(undefined);
+ await navigateToApp('app_status');
+
+ expect(await testSubjects.exists('appNotFoundPageContent')).to.eql(false);
+ expect(await testSubjects.exists('appStatusApp')).to.eql(true);
+ });
+
+ it('can change the state of the currently mounted app', async () => {
+ await setAppStatus({
+ status: AppStatus.accessible,
+ });
+
+ await navigateToApp('app_status');
+
+ expect(await testSubjects.exists('appNotFoundPageContent')).to.eql(false);
+ expect(await testSubjects.exists('appStatusApp')).to.eql(true);
+
+ await setAppStatus({
+ status: AppStatus.inaccessible,
+ });
+
+ expect(await testSubjects.exists('appNotFoundPageContent')).to.eql(true);
+ expect(await testSubjects.exists('appStatusApp')).to.eql(false);
+
+ await setAppStatus({
+ status: AppStatus.accessible,
+ });
+
+ expect(await testSubjects.exists('appNotFoundPageContent')).to.eql(false);
+ expect(await testSubjects.exists('appStatusApp')).to.eql(true);
});
});
}
diff --git a/x-pack/legacy/plugins/infra/public/components/fixed_datepicker.tsx b/x-pack/legacy/plugins/infra/public/components/fixed_datepicker.tsx
new file mode 100644
index 0000000000000..aab1bcd1da873
--- /dev/null
+++ b/x-pack/legacy/plugins/infra/public/components/fixed_datepicker.tsx
@@ -0,0 +1,25 @@
+/*
+ * 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 { EuiDatePicker, EuiDatePickerProps } from '@elastic/eui';
+import euiStyled from '../../../../common/eui_styled_components';
+
+export const FixedDatePicker = euiStyled(
+ ({
+ className,
+ inputClassName,
+ ...datePickerProps
+ }: {
+ className?: string;
+ inputClassName?: string;
+ } & EuiDatePickerProps) => (
+
+ )
+)`
+ z-index: 3 !important;
+`;
diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_timerange_form.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_timerange_form.tsx
index 4319f844b1dcc..02119fd1c09dd 100644
--- a/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_timerange_form.tsx
+++ b/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_timerange_form.tsx
@@ -5,8 +5,6 @@
*/
import {
- EuiDatePicker,
- EuiDatePickerProps,
EuiDescribedFormGroup,
EuiFlexGroup,
EuiFormControlLayout,
@@ -16,8 +14,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import moment, { Moment } from 'moment';
import React, { useMemo } from 'react';
-
-import { euiStyled } from '../../../../../../../common/eui_styled_components';
+import { FixedDatePicker } from '../../../fixed_datepicker';
const startTimeLabel = i18n.translate('xpack.infra.analysisSetup.startTimeLabel', {
defaultMessage: 'Start time',
@@ -138,18 +135,3 @@ export const AnalysisSetupTimerangeForm: React.FunctionComponent<{
);
};
-
-const FixedDatePicker = euiStyled(
- ({
- className,
- inputClassName,
- ...datePickerProps
- }: {
- className?: string;
- inputClassName?: string;
- } & EuiDatePickerProps) => (
-
- )
-)`
- z-index: 3 !important;
-`;
diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_time_controls.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_time_controls.tsx
index 8f5705a9b9c56..5095edd4c715c 100644
--- a/x-pack/legacy/plugins/infra/public/components/logging/log_time_controls.tsx
+++ b/x-pack/legacy/plugins/infra/public/components/logging/log_time_controls.tsx
@@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import moment, { Moment } from 'moment';
import React from 'react';
+import { FixedDatePicker } from '../fixed_datepicker';
const noop = () => undefined;
@@ -56,7 +57,7 @@ export class LogTimeControls extends React.PureComponent {
return (
- void;
}
diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js
index 7907131996578..6a1c5339de1f5 100644
--- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js
+++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js
@@ -96,6 +96,7 @@ export class Explorer extends React.Component {
static propTypes = {
explorerState: PropTypes.object.isRequired,
setSelectedCells: PropTypes.func.isRequired,
+ severity: PropTypes.number.isRequired,
showCharts: PropTypes.bool.isRequired,
};
@@ -260,7 +261,7 @@ export class Explorer extends React.Component {
};
render() {
- const { showCharts } = this.props;
+ const { showCharts, severity } = this.props;
const {
annotationsData,
@@ -276,7 +277,6 @@ export class Explorer extends React.Component {
queryString,
selectedCells,
selectedJobs,
- severity,
swimlaneContainerWidth,
tableData,
tableQueryString,
diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js
index 583375c87007e..a255b6b0434e4 100644
--- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js
+++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js
@@ -53,7 +53,7 @@ export const ExplorerChartSingleMetric = injectI18n(
static propTypes = {
tooManyBuckets: PropTypes.bool,
seriesConfig: PropTypes.object,
- severity: PropTypes.number,
+ severity: PropTypes.number.isRequired,
};
componentDidMount() {
@@ -312,13 +312,16 @@ export const ExplorerChartSingleMetric = injectI18n(
})
.on('mouseout', () => mlChartTooltipService.hide());
+ const isAnomalyVisible = d =>
+ _.has(d, 'anomalyScore') && Number(d.anomalyScore) >= severity;
+
// Update all dots to new positions.
dots
.attr('cx', d => lineChartXScale(d.date))
.attr('cy', d => lineChartYScale(d.value))
.attr('class', d => {
let markerClass = 'metric-value';
- if (_.has(d, 'anomalyScore') && Number(d.anomalyScore) >= severity) {
+ if (isAnomalyVisible(d)) {
markerClass += ` anomaly-marker ${getSeverityWithLow(d.anomalyScore).id}`;
}
return markerClass;
@@ -328,9 +331,7 @@ export const ExplorerChartSingleMetric = injectI18n(
const multiBucketMarkers = lineChartGroup
.select('.chart-markers')
.selectAll('.multi-bucket')
- .data(
- data.filter(d => d.anomalyScore !== null && showMultiBucketAnomalyMarker(d) === true)
- );
+ .data(data.filter(d => isAnomalyVisible(d) && showMultiBucketAnomalyMarker(d) === true));
// Remove multi-bucket markers that are no longer needed
multiBucketMarkers.exit().remove();
diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js
index 4fb4e7d4df94f..14d356c0d1c81 100644
--- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js
+++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js
@@ -280,11 +280,13 @@ export function loadViewByTopFieldValuesForSelectedTime(
const topFieldValues = [];
const topInfluencers = resp.influencers[viewBySwimlaneFieldName];
- topInfluencers.forEach(influencerData => {
- if (influencerData.maxAnomalyScore > 0) {
- topFieldValues.push(influencerData.influencerFieldValue);
- }
- });
+ if (Array.isArray(topInfluencers)) {
+ topInfluencers.forEach(influencerData => {
+ if (influencerData.maxAnomalyScore > 0) {
+ topFieldValues.push(influencerData.influencerFieldValue);
+ }
+ });
+ }
resolve(topFieldValues);
});
} else {
diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts
index b0eb1b98cd02b..4530c00c10535 100644
--- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts
+++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts
@@ -183,7 +183,7 @@ export class AdvancedJobCreator extends JobCreator {
public cloneFromExistingJob(job: Job, datafeed: Datafeed) {
this._overrideConfigs(job, datafeed);
- const detectors = getRichDetectors(job, datafeed, this.scriptFields, true);
+ const detectors = getRichDetectors(job, datafeed, this.additionalFields, true);
// keep track of the custom rules for each detector
const customRules = this._detectors.map(d => d.custom_rules);
diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts
index 7c070ccc6bc53..71619311c4361 100644
--- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts
+++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts
@@ -140,7 +140,7 @@ export class CategorizationJobCreator extends JobCreator {
public cloneFromExistingJob(job: Job, datafeed: Datafeed) {
this._overrideConfigs(job, datafeed);
this.createdBy = CREATED_BY_LABEL.CATEGORIZATION;
- const detectors = getRichDetectors(job, datafeed, this.scriptFields, false);
+ const detectors = getRichDetectors(job, datafeed, this.additionalFields, false);
const dtr = detectors[0];
if (detectors.length && dtr.agg !== null && dtr.field !== null) {
diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts
index 513c8239db01e..90c189c9d6197 100644
--- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts
+++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts
@@ -19,7 +19,7 @@ import {
CREATED_BY_LABEL,
SHARED_RESULTS_INDEX_NAME,
} from '../../../../../../common/constants/new_job';
-import { isSparseDataJob } from './util/general';
+import { isSparseDataJob, collectAggs } from './util/general';
import { parseInterval } from '../../../../../../common/util/parse_interval';
import { Calendar } from '../../../../../../common/types/calendars';
import { mlCalendarService } from '../../../../services/calendar_service';
@@ -43,6 +43,7 @@ export class JobCreator {
protected _aggs: Aggregation[] = [];
protected _fields: Field[] = [];
protected _scriptFields: Field[] = [];
+ protected _aggregationFields: Field[] = [];
protected _sparseData: boolean = false;
private _stopAllRefreshPolls: {
stop: boolean;
@@ -450,6 +451,14 @@ export class JobCreator {
return this._scriptFields;
}
+ public get aggregationFields(): Field[] {
+ return this._aggregationFields;
+ }
+
+ public get additionalFields(): Field[] {
+ return [...this._scriptFields, ...this._aggregationFields];
+ }
+
public get subscribers(): ProgressSubscriber[] {
return this._subscribers;
}
@@ -603,6 +612,7 @@ export class JobCreator {
}
this._sparseData = isSparseDataJob(job, datafeed);
+ this._scriptFields = [];
if (this._datafeed_config.script_fields !== undefined) {
this._scriptFields = Object.keys(this._datafeed_config.script_fields).map(f => ({
id: f,
@@ -610,8 +620,11 @@ export class JobCreator {
type: ES_FIELD_TYPES.KEYWORD,
aggregatable: true,
}));
- } else {
- this._scriptFields = [];
+ }
+
+ this._aggregationFields = [];
+ if (this._datafeed_config.aggregations?.buckets !== undefined) {
+ collectAggs(this._datafeed_config.aggregations.buckets, this._aggregationFields);
}
}
}
diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts
index 8a4411bf9025f..7c5fba028d9e8 100644
--- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts
+++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts
@@ -153,7 +153,7 @@ export class MultiMetricJobCreator extends JobCreator {
public cloneFromExistingJob(job: Job, datafeed: Datafeed) {
this._overrideConfigs(job, datafeed);
this.createdBy = CREATED_BY_LABEL.MULTI_METRIC;
- const detectors = getRichDetectors(job, datafeed, this.scriptFields, false);
+ const detectors = getRichDetectors(job, datafeed, this.additionalFields, false);
if (datafeed.aggregations !== undefined) {
// if we've converting from a single metric job,
diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts
index 9300e53c578e1..3009d68ca67ca 100644
--- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts
+++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts
@@ -135,7 +135,7 @@ export class PopulationJobCreator extends JobCreator {
public cloneFromExistingJob(job: Job, datafeed: Datafeed) {
this._overrideConfigs(job, datafeed);
this.createdBy = CREATED_BY_LABEL.POPULATION;
- const detectors = getRichDetectors(job, datafeed, this.scriptFields, false);
+ const detectors = getRichDetectors(job, datafeed, this.additionalFields, false);
this.removeAllDetectors();
diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts
index f98fd4dbe970a..9f3500185c2bf 100644
--- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts
+++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts
@@ -190,7 +190,7 @@ export class SingleMetricJobCreator extends JobCreator {
public cloneFromExistingJob(job: Job, datafeed: Datafeed) {
this._overrideConfigs(job, datafeed);
this.createdBy = CREATED_BY_LABEL.SINGLE_METRIC;
- const detectors = getRichDetectors(job, datafeed, this.scriptFields, false);
+ const detectors = getRichDetectors(job, datafeed, this.additionalFields, false);
this.removeAllDetectors();
diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts
index 6443539a9877d..e5b6212a4326e 100644
--- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts
+++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts
@@ -11,7 +11,8 @@ import {
ML_JOB_AGGREGATION,
SPARSE_DATA_AGGREGATIONS,
} from '../../../../../../../common/constants/aggregation_types';
-import { MLCATEGORY } from '../../../../../../../common/constants/field_types';
+import { MLCATEGORY, DOC_COUNT } from '../../../../../../../common/constants/field_types';
+import { ES_FIELD_TYPES } from '../../../../../../../../../../../src/plugins/data/public';
import {
EVENT_RATE_FIELD_ID,
Field,
@@ -27,14 +28,14 @@ import {
} from '../index';
import { CREATED_BY_LABEL, JOB_TYPE } from '../../../../../../../common/constants/new_job';
-const getFieldByIdFactory = (scriptFields: Field[]) => (id: string) => {
+const getFieldByIdFactory = (additionalFields: Field[]) => (id: string) => {
let field = newJobCapsService.getFieldById(id);
// if no field could be found it may be a pretend field, like mlcategory or a script field
if (field === null) {
if (id === MLCATEGORY) {
field = mlCategory;
- } else if (scriptFields.length) {
- field = scriptFields.find(f => f.id === id) || null;
+ } else if (additionalFields.length) {
+ field = additionalFields.find(f => f.id === id) || null;
}
}
return field;
@@ -44,12 +45,12 @@ const getFieldByIdFactory = (scriptFields: Field[]) => (id: string) => {
export function getRichDetectors(
job: Job,
datafeed: Datafeed,
- scriptFields: Field[],
+ additionalFields: Field[],
advanced: boolean = false
) {
const detectors = advanced ? getDetectorsAdvanced(job, datafeed) : getDetectors(job, datafeed);
- const getFieldById = getFieldByIdFactory(scriptFields);
+ const getFieldById = getFieldByIdFactory(additionalFields);
return detectors.map(d => {
let field = null;
@@ -82,19 +83,19 @@ export function getRichDetectors(
});
}
-export function createFieldOptions(fields: Field[]) {
- return fields
- .filter(f => f.id !== EVENT_RATE_FIELD_ID)
- .map(f => ({
- label: f.name,
- }))
- .sort((a, b) => a.label.localeCompare(b.label));
-}
-
-export function createScriptFieldOptions(scriptFields: Field[]) {
- return scriptFields.map(f => ({
- label: f.id,
- }));
+export function createFieldOptions(fields: Field[], additionalFields: Field[]) {
+ return [
+ ...fields
+ .filter(f => f.id !== EVENT_RATE_FIELD_ID)
+ .map(f => ({
+ label: f.name,
+ })),
+ ...additionalFields
+ .filter(f => fields.some(f2 => f2.id === f.id) === false)
+ .map(f => ({
+ label: f.id,
+ })),
+ ].sort((a, b) => a.label.localeCompare(b.label));
}
export function createMlcategoryFieldOption(categorizationFieldName: string | null) {
@@ -108,6 +109,16 @@ export function createMlcategoryFieldOption(categorizationFieldName: string | nu
];
}
+export function createDocCountFieldOption(usingAggregations: boolean) {
+ return usingAggregations
+ ? [
+ {
+ label: DOC_COUNT,
+ },
+ ]
+ : [];
+}
+
function getDetectorsAdvanced(job: Job, datafeed: Datafeed) {
return processFieldlessAggs(job.analysis_config.detectors);
}
@@ -305,3 +316,26 @@ export function getJobCreatorTitle(jobCreator: JobCreatorType) {
return '';
}
}
+
+// recurse through a datafeed aggregation object,
+// adding top level keys from each nested agg to an array
+// of fields
+export function collectAggs(o: any, aggFields: Field[]) {
+ for (const i in o) {
+ if (o[i] !== null && typeof o[i] === 'object') {
+ if (i === 'aggregations' || i === 'aggs') {
+ Object.keys(o[i]).forEach(k => {
+ if (k !== 'aggregations' && i !== 'aggs') {
+ aggFields.push({
+ id: k,
+ name: k,
+ type: ES_FIELD_TYPES.KEYWORD,
+ aggregatable: true,
+ });
+ }
+ });
+ }
+ collectAggs(o[i], aggFields);
+ }
+ }
+}
diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx
index f2e2516866835..9af1226d1fe6c 100644
--- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx
+++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx
@@ -4,9 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { FC } from 'react';
+import React, { FC, useContext } from 'react';
import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui';
+import { JobCreatorContext } from '../../../job_creator_context';
import { Field } from '../../../../../../../../../common/types/fields';
import { createFieldOptions } from '../../../../../common/job_creator/util/general';
@@ -17,7 +18,8 @@ interface Props {
}
export const TimeFieldSelect: FC = ({ fields, changeHandler, selectedField }) => {
- const options: EuiComboBoxOptionProps[] = createFieldOptions(fields);
+ const { jobCreator } = useContext(JobCreatorContext);
+ const options: EuiComboBoxOptionProps[] = createFieldOptions(fields, jobCreator.additionalFields);
const selection: EuiComboBoxOptionProps[] = [];
if (selectedField !== null) {
diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx
index 06c8068a9c005..753cea7adcb35 100644
--- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx
+++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx
@@ -18,7 +18,6 @@ import { JobCreatorContext } from '../../../job_creator_context';
import { AdvancedJobCreator } from '../../../../../common/job_creator';
import {
createFieldOptions,
- createScriptFieldOptions,
createMlcategoryFieldOption,
} from '../../../../../common/job_creator/util/general';
import {
@@ -88,7 +87,7 @@ export const AdvancedDetectorModal: FC = ({
const [fieldOptionEnabled, setFieldOptionEnabled] = useState(true);
const { descriptionPlaceholder, setDescriptionPlaceholder } = useDetectorPlaceholder(detector);
- const usingScriptFields = jobCreator.scriptFields.length > 0;
+ const usingScriptFields = jobCreator.additionalFields.length > 0;
// list of aggregation combobox options.
const aggOptions: EuiComboBoxOptionProps[] = aggs
@@ -98,12 +97,12 @@ export const AdvancedDetectorModal: FC = ({
// fields available for the selected agg
const { currentFieldOptions, setCurrentFieldOptions } = useCurrentFieldOptions(
detector.agg,
- jobCreator.scriptFields
+ jobCreator.additionalFields,
+ fields
);
const allFieldOptions: EuiComboBoxOptionProps[] = [
- ...createFieldOptions(fields),
- ...createScriptFieldOptions(jobCreator.scriptFields),
+ ...createFieldOptions(fields, jobCreator.additionalFields),
].sort(comboBoxOptionsSort);
const splitFieldOptions: EuiComboBoxOptionProps[] = [
@@ -127,7 +126,9 @@ export const AdvancedDetectorModal: FC = ({
return mlCategory;
}
return (
- fields.find(f => f.id === title) || jobCreator.scriptFields.find(f => f.id === title) || null
+ fields.find(f => f.id === title) ||
+ jobCreator.additionalFields.find(f => f.id === title) ||
+ null
);
}
@@ -365,21 +366,27 @@ function useDetectorPlaceholder(detector: RichDetector) {
}
// creates list of combobox options based on an aggregation's field list
-function createFieldOptionsFromAgg(agg: Aggregation | null) {
- return createFieldOptions(agg !== null && agg.fields !== undefined ? agg.fields : []);
+function createFieldOptionsFromAgg(agg: Aggregation | null, additionalFields: Field[]) {
+ return createFieldOptions(
+ agg !== null && agg.fields !== undefined ? agg.fields : [],
+ additionalFields
+ );
}
// custom hook for storing combobox options based on an aggregation field list
-function useCurrentFieldOptions(aggregation: Aggregation | null, scriptFields: Field[]) {
+function useCurrentFieldOptions(
+ aggregation: Aggregation | null,
+ additionalFields: Field[],
+ fields: Field[]
+) {
const [currentFieldOptions, setCurrentFieldOptions] = useState(
- createFieldOptionsFromAgg(aggregation)
+ createFieldOptionsFromAgg(aggregation, additionalFields)
);
- const scriptFieldOptions = createScriptFieldOptions(scriptFields);
return {
currentFieldOptions,
setCurrentFieldOptions: (agg: Aggregation | null) =>
- setCurrentFieldOptions([...createFieldOptionsFromAgg(agg), ...scriptFieldOptions]),
+ setCurrentFieldOptions(createFieldOptionsFromAgg(agg, additionalFields)),
};
}
diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx
index d995d40284aba..6451c2785eae0 100644
--- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx
+++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx
@@ -9,10 +9,7 @@ import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui';
import { JobCreatorContext } from '../../../job_creator_context';
import { Field } from '../../../../../../../../../common/types/fields';
-import {
- createFieldOptions,
- createScriptFieldOptions,
-} from '../../../../../common/job_creator/util/general';
+import { createFieldOptions } from '../../../../../common/job_creator/util/general';
interface Props {
fields: Field[];
@@ -23,8 +20,7 @@ interface Props {
export const CategorizationFieldSelect: FC = ({ fields, changeHandler, selectedField }) => {
const { jobCreator } = useContext(JobCreatorContext);
const options: EuiComboBoxOptionProps[] = [
- ...createFieldOptions(fields),
- ...createScriptFieldOptions(jobCreator.scriptFields),
+ ...createFieldOptions(fields, jobCreator.additionalFields),
];
const selection: EuiComboBoxOptionProps[] = [];
diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx
index 639bdb9ec76bf..d4ac470f4ea4f 100644
--- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx
+++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx
@@ -11,7 +11,6 @@ import { JobCreatorContext } from '../../../job_creator_context';
import { Field } from '../../../../../../../../../common/types/fields';
import {
createFieldOptions,
- createScriptFieldOptions,
createMlcategoryFieldOption,
} from '../../../../../common/job_creator/util/general';
@@ -24,8 +23,7 @@ interface Props {
export const InfluencersSelect: FC = ({ fields, changeHandler, selectedInfluencers }) => {
const { jobCreator } = useContext(JobCreatorContext);
const options: EuiComboBoxOptionProps[] = [
- ...createFieldOptions(fields),
- ...createScriptFieldOptions(jobCreator.scriptFields),
+ ...createFieldOptions(fields, jobCreator.additionalFields),
...createMlcategoryFieldOption(jobCreator.categorizationFieldName),
];
diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx
index efe32e3173cad..6fe3aaf0a8652 100644
--- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx
+++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx
@@ -11,7 +11,7 @@ import { JobCreatorContext } from '../../../job_creator_context';
import { Field } from '../../../../../../../../../common/types/fields';
import {
createFieldOptions,
- createScriptFieldOptions,
+ createDocCountFieldOption,
} from '../../../../../common/job_creator/util/general';
interface Props {
@@ -23,8 +23,8 @@ interface Props {
export const SummaryCountFieldSelect: FC = ({ fields, changeHandler, selectedField }) => {
const { jobCreator } = useContext(JobCreatorContext);
const options: EuiComboBoxOptionProps[] = [
- ...createFieldOptions(fields),
- ...createScriptFieldOptions(jobCreator.scriptFields),
+ ...createFieldOptions(fields, jobCreator.additionalFields),
+ ...createDocCountFieldOption(jobCreator.aggregationFields.length > 0),
];
const selection: EuiComboBoxOptionProps[] = [];
diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx
index 6aaad5294369b..633efc2856dac 100644
--- a/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx
+++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx
@@ -184,6 +184,7 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim
explorerState,
setSelectedCells,
showCharts,
+ severity: tableSeverity.val,
}}
/>
diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx
index c3c644d43fa59..f824faf7845c6 100644
--- a/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx
+++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx
@@ -26,7 +26,10 @@ import { APP_STATE_ACTION } from '../../timeseriesexplorer/timeseriesexplorer_co
import {
createTimeSeriesJobData,
getAutoZoomDuration,
+ validateJobSelection,
} from '../../timeseriesexplorer/timeseriesexplorer_utils';
+import { TimeSeriesExplorerPage } from '../../timeseriesexplorer/timeseriesexplorer_page';
+import { TimeseriesexplorerNoJobsFound } from '../../timeseriesexplorer/components/timeseriesexplorer_no_jobs_found';
import { useUrlState } from '../../util/url_state';
import { useTableInterval } from '../../components/controls/select_interval';
import { useTableSeverity } from '../../components/controls/select_severity';
@@ -81,6 +84,7 @@ export const TimeSeriesExplorerUrlStateManager: FC();
const refresh = useRefresh();
useEffect(() => {
@@ -141,6 +145,10 @@ export const TimeSeriesExplorerUrlStateManager: FC
+
+
+ );
+ }
+
+ if (selectedJobId === undefined || autoZoomDuration === undefined || bounds === undefined) {
+ return null;
+ }
+
return (
{
- let jobFilter = {};
- // if no jobId specified, load all of the messages
- if (jobId !== undefined) {
- jobFilter = {
- bool: {
- should: [
- {
- term: {
- job_id: '', // catch system messages
- },
- },
- {
- term: {
- job_id: jobId, // messages for specified jobId
- },
- },
- ],
- },
- };
- }
-
- let timeFilter = {};
- if (fromRange !== undefined && fromRange !== '') {
- timeFilter = {
- range: {
- timestamp: {
- gte: `now-${fromRange}`,
- lte: 'now',
- },
- },
- };
- }
-
- ml.esSearch({
- index: ML_NOTIFICATION_INDEX_PATTERN,
- ignore_unavailable: true,
- rest_total_hits_as_int: true,
- size: 1000,
- body: {
- sort: [{ timestamp: { order: 'asc' } }, { job_id: { order: 'asc' } }],
- query: {
- bool: {
- filter: [
- {
- bool: {
- must_not: {
- term: {
- level: 'activity',
- },
- },
- },
- },
- anomalyDetectorTypeFilter,
- jobFilter,
- timeFilter,
- ],
- },
- },
- },
- })
- .then(resp => {
- let messages = [];
- if (resp.hits.total !== 0) {
- messages = resp.hits.hits.map(hit => hit._source);
- }
- resolve({ messages });
- })
- .catch(resp => {
- reject(resp);
- });
- });
-}
-
-// search highest, most recent audit messages for all jobs for the last 24hrs.
-function getAuditMessagesSummary() {
- return new Promise((resolve, reject) => {
- ml.esSearch({
- index: ML_NOTIFICATION_INDEX_PATTERN,
- ignore_unavailable: true,
- rest_total_hits_as_int: true,
- size: 0,
- body: {
- query: {
- bool: {
- filter: [
- {
- range: {
- timestamp: {
- gte: 'now-1d',
- },
- },
- },
- anomalyDetectorTypeFilter,
- ],
- },
- },
- aggs: {
- levelsPerJob: {
- terms: {
- field: 'job_id',
- },
- aggs: {
- levels: {
- terms: {
- field: 'level',
- },
- aggs: {
- latestMessage: {
- terms: {
- field: 'message.raw',
- size: 1,
- order: {
- latestMessage: 'desc',
- },
- },
- aggs: {
- latestMessage: {
- max: {
- field: 'timestamp',
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- })
- .then(resp => {
- let messagesPerJob = [];
- if (
- resp.hits.total !== 0 &&
- resp.aggregations &&
- resp.aggregations.levelsPerJob &&
- resp.aggregations.levelsPerJob.buckets &&
- resp.aggregations.levelsPerJob.buckets.length
- ) {
- messagesPerJob = resp.aggregations.levelsPerJob.buckets;
- }
- resolve({ messagesPerJob });
- })
- .catch(resp => {
- reject(resp);
- });
- });
-}
-
-export const jobMessagesService = {
- getJobAuditMessages,
- getAuditMessagesSummary,
-};
diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/index.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/index.ts
similarity index 100%
rename from x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/index.js
rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/index.ts
diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.tsx
similarity index 100%
rename from x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.js
rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.tsx
diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts
index 651c609004236..6c1bb01137c91 100644
--- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts
+++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts
@@ -12,12 +12,11 @@ import { getDateFormatTz, TimeRangeBounds } from '../explorer/explorer_utils';
declare const TimeSeriesExplorer: FC<{
appStateHandler: (action: string, payload: any) => void;
- autoZoomDuration?: number;
- bounds?: TimeRangeBounds;
+ autoZoomDuration: number;
+ bounds: TimeRangeBounds;
dateFormatTz: string;
- jobsWithTimeRange: any[];
lastRefresh: number;
- selectedJobIds: string[];
+ selectedJobId: string;
selectedDetectorIndex: number;
selectedEntities: any[];
selectedForecastId: string;
diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js
index 1862ead045743..f3d8692bfb3e9 100644
--- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js
+++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js
@@ -8,7 +8,7 @@
* React component for rendering Single Metric Viewer.
*/
-import { debounce, difference, each, find, get, has, isEqual, without } from 'lodash';
+import { debounce, each, find, get, has, isEqual } from 'lodash';
import moment from 'moment-timezone';
import { Subject, Subscription, forkJoin } from 'rxjs';
import { map, debounceTime, switchMap, tap, withLatestFrom } from 'rxjs/operators';
@@ -24,7 +24,6 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
- EuiProgress,
EuiSelect,
EuiSpacer,
EuiText,
@@ -49,15 +48,12 @@ import { AnomaliesTable } from '../components/anomalies_table/anomalies_table';
import { ChartTooltip } from '../components/chart_tooltip';
import { EntityControl } from './components/entity_control';
import { ForecastingModal } from './components/forecasting_modal/forecasting_modal';
-import { JobSelector } from '../components/job_selector';
-import { getTimeRangeFromSelection } from '../components/job_selector/job_select_service_utils';
import { LoadingIndicator } from '../components/loading_indicator/loading_indicator';
-import { NavigationMenu } from '../components/navigation_menu';
import { SelectInterval } from '../components/controls/select_interval/select_interval';
import { SelectSeverity } from '../components/controls/select_severity/select_severity';
import { TimeseriesChart } from './components/timeseries_chart/timeseries_chart';
-import { TimeseriesexplorerNoJobsFound } from './components/timeseriesexplorer_no_jobs_found';
import { TimeseriesexplorerNoChartData } from './components/timeseriesexplorer_no_chart_data';
+import { TimeSeriesExplorerPage } from './timeseriesexplorer_page';
import { ml } from '../services/ml_api_service';
import { mlFieldFormatService } from '../services/field_format_service';
@@ -154,44 +150,16 @@ function getTimeseriesexplorerDefaultState() {
};
}
-const TimeSeriesExplorerPage = ({ children, jobSelectorProps, loading, resizeRef }) => (
-
-
- {/* Show animated progress bar while loading */}
- {loading && }
- {/* Show a progress bar with progress 0% when not loading.
- If we'd just show no progress bar when not loading it would result in a flickering height effect. */}
- {!loading && (
-
- )}
-
-
- {children}
-
-
-);
-
const containerPadding = 24;
export class TimeSeriesExplorer extends React.Component {
static propTypes = {
appStateHandler: PropTypes.func.isRequired,
- autoZoomDuration: PropTypes.number,
- bounds: PropTypes.object,
+ autoZoomDuration: PropTypes.number.isRequired,
+ bounds: PropTypes.object.isRequired,
dateFormatTz: PropTypes.string.isRequired,
- jobsWithTimeRange: PropTypes.array.isRequired,
lastRefresh: PropTypes.number.isRequired,
- selectedJobIds: PropTypes.arrayOf(PropTypes.string),
+ selectedJobId: PropTypes.string.isRequired,
selectedDetectorIndex: PropTypes.number,
selectedEntities: PropTypes.object,
selectedForecastId: PropTypes.string,
@@ -285,9 +253,9 @@ export class TimeSeriesExplorer extends React.Component {
contextChartSelectedInitCallDone = false;
getFocusAggregationInterval(selection) {
- const { selectedJobIds } = this.props;
+ const { selectedJobId } = this.props;
const jobs = createTimeSeriesJobData(mlJobService.jobs);
- const selectedJob = mlJobService.getJob(selectedJobIds[0]);
+ const selectedJob = mlJobService.getJob(selectedJobId);
// Calculate the aggregation interval for the focus chart.
const bounds = { min: moment(selection.from), max: moment(selection.to) };
@@ -299,9 +267,9 @@ export class TimeSeriesExplorer extends React.Component {
* Gets focus data for the current component state/
*/
getFocusData(selection) {
- const { selectedJobIds, selectedForecastId, selectedDetectorIndex } = this.props;
+ const { selectedJobId, selectedForecastId, selectedDetectorIndex } = this.props;
const { modelPlotEnabled } = this.state;
- const selectedJob = mlJobService.getJob(selectedJobIds[0]);
+ const selectedJob = mlJobService.getJob(selectedJobId);
const entityControls = this.getControlsForDetector();
// Calculate the aggregation interval for the focus chart.
@@ -356,11 +324,11 @@ export class TimeSeriesExplorer extends React.Component {
const {
dateFormatTz,
selectedDetectorIndex,
- selectedJobIds,
+ selectedJobId,
tableInterval,
tableSeverity,
} = this.props;
- const selectedJob = mlJobService.getJob(selectedJobIds[0]);
+ const selectedJob = mlJobService.getJob(selectedJobId);
const entityControls = this.getControlsForDetector();
return ml.results
@@ -424,8 +392,8 @@ export class TimeSeriesExplorer extends React.Component {
loadEntityValues = async (entities, searchTerm = {}) => {
this.setState({ entitiesLoading: true });
- const { bounds, selectedJobIds, selectedDetectorIndex } = this.props;
- const selectedJob = mlJobService.getJob(selectedJobIds[0]);
+ const { bounds, selectedJobId, selectedDetectorIndex } = this.props;
+ const selectedJob = mlJobService.getJob(selectedJobId);
// Populate the entity input datalists with the values from the top records by score
// for the selected detector across the full time range. No need to pass through finish().
@@ -479,17 +447,13 @@ export class TimeSeriesExplorer extends React.Component {
bounds,
selectedDetectorIndex,
selectedForecastId,
- selectedJobIds,
+ selectedJobId,
zoom,
} = this.props;
- if (selectedJobIds === undefined || bounds === undefined) {
- return;
- }
-
const { loadCounter: currentLoadCounter } = this.state;
- const currentSelectedJob = mlJobService.getJob(selectedJobIds[0]);
+ const currentSelectedJob = mlJobService.getJob(selectedJobId);
if (currentSelectedJob === undefined) {
return;
@@ -526,7 +490,7 @@ export class TimeSeriesExplorer extends React.Component {
const { loadCounter, modelPlotEnabled } = this.state;
const jobs = createTimeSeriesJobData(mlJobService.jobs);
- const selectedJob = mlJobService.getJob(selectedJobIds[0]);
+ const selectedJob = mlJobService.getJob(selectedJobId);
const detectorIndex = selectedDetectorIndex;
let awaitingCount = 3;
@@ -717,8 +681,8 @@ export class TimeSeriesExplorer extends React.Component {
* @param callback to invoke after a state update.
*/
getControlsForDetector = () => {
- const { selectedDetectorIndex, selectedEntities, selectedJobIds } = this.props;
- const selectedJob = mlJobService.getJob(selectedJobIds[0]);
+ const { selectedDetectorIndex, selectedEntities, selectedJobId } = this.props;
+ const selectedJob = mlJobService.getJob(selectedJobId);
const entities = [];
@@ -871,9 +835,9 @@ export class TimeSeriesExplorer extends React.Component {
}
}),
switchMap(selection => {
- const { selectedJobIds } = this.props;
+ const { selectedJobId } = this.props;
const jobs = createTimeSeriesJobData(mlJobService.jobs);
- const selectedJob = mlJobService.getJob(selectedJobIds[0]);
+ const selectedJob = mlJobService.getJob(selectedJobId);
// Calculate the aggregation interval for the focus chart.
const bounds = { min: moment(selection.from), max: moment(selection.to) };
@@ -927,133 +891,19 @@ export class TimeSeriesExplorer extends React.Component {
this.componentDidUpdate();
}
- /**
- * returns true/false if setGlobalState has been triggered
- * or returns the job id which should be loaded.
- */
- checkJobSelection() {
- const { jobsWithTimeRange, selectedJobIds, setGlobalState } = this.props;
-
- const jobs = createTimeSeriesJobData(mlJobService.jobs);
- const timeSeriesJobIds = jobs.map(j => j.id);
-
- // Check if any of the jobs set in the URL are not time series jobs
- // (e.g. if switching to this view straight from the Anomaly Explorer).
- const invalidIds = difference(selectedJobIds, timeSeriesJobIds);
- const validSelectedJobIds = without(selectedJobIds, ...invalidIds);
- if (invalidIds.length > 0) {
- let warningText = i18n.translate(
- 'xpack.ml.timeSeriesExplorer.canNotViewRequestedJobsWarningMessage',
- {
- defaultMessage: `You can't view requested {invalidIdsCount, plural, one {job} other {jobs}} {invalidIds} in this dashboard`,
- values: {
- invalidIdsCount: invalidIds.length,
- invalidIds,
- },
- }
- );
- if (validSelectedJobIds.length === 0 && timeSeriesJobIds.length > 0) {
- warningText += i18n.translate('xpack.ml.timeSeriesExplorer.autoSelectingFirstJobText', {
- defaultMessage: ', auto selecting first job',
- });
- }
- toastNotifications.addWarning(warningText);
- }
-
- if (validSelectedJobIds.length > 1) {
- // if more than one job or a group has been loaded from the URL
- if (validSelectedJobIds.length > 1) {
- // if more than one job, select the first job from the selection.
- toastNotifications.addWarning(
- i18n.translate('xpack.ml.timeSeriesExplorer.youCanViewOneJobAtTimeWarningMessage', {
- defaultMessage: 'You can only view one job at a time in this dashboard',
- })
- );
- setGlobalState('ml', { jobIds: [validSelectedJobIds[0]] });
- return true;
- } else {
- // if a group has been loaded
- if (selectedJobIds.length > 0) {
- // if the group contains valid jobs, select the first
- toastNotifications.addWarning(
- i18n.translate('xpack.ml.timeSeriesExplorer.youCanViewOneJobAtTimeWarningMessage', {
- defaultMessage: 'You can only view one job at a time in this dashboard',
- })
- );
- setGlobalState('ml', { jobIds: [validSelectedJobIds[0]] });
- return true;
- } else if (jobs.length > 0) {
- // if there are no valid jobs in the group but there are valid jobs
- // in the list of all jobs, select the first
- const jobIds = [jobs[0].id];
- const time = getTimeRangeFromSelection(jobsWithTimeRange, jobIds);
- setGlobalState({
- ...{ ml: { jobIds } },
- ...(time !== undefined ? { time } : {}),
- });
- return true;
- } else {
- // if there are no valid jobs left.
- return false;
- }
- }
- } else if (invalidIds.length > 0 && validSelectedJobIds.length > 0) {
- // if some ids have been filtered out because they were invalid.
- // refresh the URL with the first valid id
- setGlobalState('ml', { jobIds: [validSelectedJobIds[0]] });
- return true;
- } else if (validSelectedJobIds.length > 0) {
- // normal behavior. a job ID has been loaded from the URL
- // Clear the detectorIndex, entities and forecast info.
- return validSelectedJobIds[0];
- } else {
- if (validSelectedJobIds.length === 0 && jobs.length > 0) {
- // no jobs were loaded from the URL, so add the first job
- // from the full jobs list.
- const jobIds = [jobs[0].id];
- const time = getTimeRangeFromSelection(jobsWithTimeRange, jobIds);
- setGlobalState({
- ...{ ml: { jobIds } },
- ...(time !== undefined ? { time } : {}),
- });
- return true;
- } else {
- // Jobs exist, but no time series jobs.
- return false;
- }
- }
- }
-
componentDidUpdate(previousProps) {
- if (
- previousProps === undefined ||
- !isEqual(previousProps.selectedJobIds, this.props.selectedJobIds)
- ) {
- const update = this.checkJobSelection();
- // - true means a setGlobalState got triggered and
- // we'll just wait for the next React render.
- // - false means there are either no jobs or no time based jobs present.
- // - if we get back a string it means we got back a job id we can load.
- if (update === true) {
- return;
- } else if (update === false) {
- this.setState({ loading: false });
- return;
- } else if (typeof update === 'string') {
- this.contextChartSelectedInitCallDone = false;
- this.setState({ fullRefresh: false, loading: true }, () => {
- this.loadForJobId(update);
- });
- }
+ if (previousProps === undefined || previousProps.selectedJobId !== this.props.selectedJobId) {
+ this.contextChartSelectedInitCallDone = false;
+ this.setState({ fullRefresh: false, loading: true }, () => {
+ this.loadForJobId(this.props.selectedJobId);
+ });
}
if (
- this.props.bounds !== undefined &&
- this.props.selectedJobIds !== undefined &&
- (previousProps === undefined ||
- !isEqual(previousProps.selectedJobIds, this.props.selectedJobIds) ||
- previousProps.selectedDetectorIndex !== this.props.selectedDetectorIndex ||
- !isEqual(previousProps.selectedEntities, this.props.selectedEntities))
+ previousProps === undefined ||
+ previousProps.selectedJobId !== this.props.selectedJobId ||
+ previousProps.selectedDetectorIndex !== this.props.selectedDetectorIndex ||
+ !isEqual(previousProps.selectedEntities, this.props.selectedEntities)
) {
const entityControls = this.getControlsForDetector();
this.loadEntityValues(entityControls);
@@ -1076,7 +926,7 @@ export class TimeSeriesExplorer extends React.Component {
!isEqual(previousProps.selectedDetectorIndex, this.props.selectedDetectorIndex) ||
!isEqual(previousProps.selectedEntities, this.props.selectedEntities) ||
!isEqual(previousProps.selectedForecastId, this.props.selectedForecastId) ||
- !isEqual(previousProps.selectedJobIds, this.props.selectedJobIds) ||
+ previousProps.selectedJobId !== this.props.selectedJobId ||
!isEqual(previousProps.zoom, this.props.zoom)
) {
const fullRefresh =
@@ -1086,7 +936,7 @@ export class TimeSeriesExplorer extends React.Component {
!isEqual(previousProps.selectedDetectorIndex, this.props.selectedDetectorIndex) ||
!isEqual(previousProps.selectedEntities, this.props.selectedEntities) ||
!isEqual(previousProps.selectedForecastId, this.props.selectedForecastId) ||
- !isEqual(previousProps.selectedJobIds, this.props.selectedJobIds);
+ previousProps.selectedJobId !== this.props.selectedJobId;
this.loadSingleMetricData(fullRefresh);
}
@@ -1159,7 +1009,7 @@ export class TimeSeriesExplorer extends React.Component {
dateFormatTz,
lastRefresh,
selectedDetectorIndex,
- selectedJobIds,
+ selectedJobId,
} = this.props;
const {
@@ -1211,34 +1061,13 @@ export class TimeSeriesExplorer extends React.Component {
autoZoomDuration,
};
- const jobSelectorProps = {
- dateFormatTz,
- singleSelection: true,
- timeseriesOnly: true,
- };
-
const jobs = createTimeSeriesJobData(mlJobService.jobs);
- if (jobs.length === 0) {
- return (
-
-
-
- );
- }
-
- if (
- selectedJobIds === undefined ||
- selectedJobIds.length > 1 ||
- selectedDetectorIndex === undefined ||
- mlJobService.getJob(selectedJobIds[0]) === undefined
- ) {
- return (
-
- );
+ if (selectedDetectorIndex === undefined || mlJobService.getJob(selectedJobId) === undefined) {
+ return ;
}
- const selectedJob = mlJobService.getJob(selectedJobIds[0]);
+ const selectedJob = mlJobService.getJob(selectedJobId);
const entityControls = this.getControlsForDetector();
const fieldNamesWithEmptyValues = entityControls
@@ -1280,7 +1109,7 @@ export class TimeSeriesExplorer extends React.Component {
return (
diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_page.tsx b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_page.tsx
new file mode 100644
index 0000000000000..9da1a79232fce
--- /dev/null
+++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_page.tsx
@@ -0,0 +1,54 @@
+/*
+ * 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 } from 'react';
+
+import { EuiProgress } from '@elastic/eui';
+
+import { JobSelector } from '../components/job_selector';
+import { NavigationMenu } from '../components/navigation_menu';
+
+interface TimeSeriesExplorerPageProps {
+ dateFormatTz: string;
+ loading?: boolean;
+ resizeRef?: any;
+}
+
+export const TimeSeriesExplorerPage: FC = ({
+ children,
+ dateFormatTz,
+ loading,
+ resizeRef,
+}) => {
+ return (
+ <>
+
+ {/* Show animated progress bar while loading */}
+ {loading === true && (
+
+ )}
+ {/* Show a progress bar with progress 0% when not loading.
+ If we'd just show no progress bar when not loading it would result in a flickering height effect. */}
+ {loading === false && (
+
+ )}
+
+
+ {children}
+
+ >
+ );
+};
diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/index.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/index.ts
index 578dbdf1277a0..dcfbe94c97cc6 100644
--- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/index.ts
+++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/index.ts
@@ -6,3 +6,4 @@
export { getFocusData } from './get_focus_data';
export * from './timeseriesexplorer_utils';
+export { validateJobSelection } from './validate_job_selection';
diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.ts
new file mode 100644
index 0000000000000..f1cdaf3ba8c1b
--- /dev/null
+++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.ts
@@ -0,0 +1,87 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { difference, without } from 'lodash';
+
+import { i18n } from '@kbn/i18n';
+
+import { toastNotifications } from 'ui/notify';
+
+import { MlJobWithTimeRange } from '../../../../common/types/jobs';
+
+import { getTimeRangeFromSelection } from '../../components/job_selector/job_select_service_utils';
+import { mlJobService } from '../../services/job_service';
+
+import { createTimeSeriesJobData } from './timeseriesexplorer_utils';
+
+/**
+ * returns true/false if setGlobalState has been triggered
+ * or returns the job id which should be loaded.
+ */
+export function validateJobSelection(
+ jobsWithTimeRange: MlJobWithTimeRange[],
+ selectedJobIds: string[],
+ setGlobalState: (...args: any) => void
+) {
+ const jobs = createTimeSeriesJobData(mlJobService.jobs);
+ const timeSeriesJobIds: string[] = jobs.map((j: any) => j.id);
+
+ // Check if any of the jobs set in the URL are not time series jobs
+ // (e.g. if switching to this view straight from the Anomaly Explorer).
+ const invalidIds: string[] = difference(selectedJobIds, timeSeriesJobIds);
+ const validSelectedJobIds = without(selectedJobIds, ...invalidIds);
+ if (invalidIds.length > 0) {
+ let warningText = i18n.translate(
+ 'xpack.ml.timeSeriesExplorer.canNotViewRequestedJobsWarningMessage',
+ {
+ defaultMessage: `You can't view requested {invalidIdsCount, plural, one {job} other {jobs}} {invalidIds} in this dashboard`,
+ values: {
+ invalidIdsCount: invalidIds.length,
+ invalidIds: invalidIds.join(', '),
+ },
+ }
+ );
+ if (validSelectedJobIds.length === 0 && timeSeriesJobIds.length > 0) {
+ warningText += i18n.translate('xpack.ml.timeSeriesExplorer.autoSelectingFirstJobText', {
+ defaultMessage: ', auto selecting first job',
+ });
+ }
+ toastNotifications.addWarning(warningText);
+ }
+
+ if (validSelectedJobIds.length > 1) {
+ // if more than one job, select the first job from the selection.
+ toastNotifications.addWarning(
+ i18n.translate('xpack.ml.timeSeriesExplorer.youCanViewOneJobAtTimeWarningMessage', {
+ defaultMessage: 'You can only view one job at a time in this dashboard',
+ })
+ );
+ setGlobalState('ml', { jobIds: [validSelectedJobIds[0]] });
+ return true;
+ } else if (invalidIds.length > 0 && validSelectedJobIds.length > 0) {
+ // if some ids have been filtered out because they were invalid.
+ // refresh the URL with the first valid id
+ setGlobalState('ml', { jobIds: [validSelectedJobIds[0]] });
+ return true;
+ } else if (validSelectedJobIds.length === 1) {
+ // normal behavior. a job ID has been loaded from the URL
+ // Clear the detectorIndex, entities and forecast info.
+ return validSelectedJobIds[0];
+ } else if (validSelectedJobIds.length === 0 && jobs.length > 0) {
+ // no jobs were loaded from the URL, so add the first job
+ // from the full jobs list.
+ const jobIds = [jobs[0].id];
+ const time = getTimeRangeFromSelection(jobsWithTimeRange, jobIds);
+ setGlobalState({
+ ...{ ml: { jobIds } },
+ ...(time !== undefined ? { time } : {}),
+ });
+ return true;
+ } else {
+ // Jobs exist, but no time series jobs.
+ return false;
+ }
+}
diff --git a/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js b/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js
index c9cc8a3da574a..52495b3b732d0 100644
--- a/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js
+++ b/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js
@@ -10,6 +10,30 @@ import moment from 'moment';
const SIZE = 1000;
const LEVEL = { system_info: -1, info: 0, warning: 1, error: 2 };
+// filter to match job_type: 'anomaly_detector' or no job_type field at all
+// if no job_type field exist, we can assume the message is for an anomaly detector job
+const anomalyDetectorTypeFilter = {
+ bool: {
+ should: [
+ {
+ term: {
+ job_type: 'anomaly_detector',
+ },
+ },
+ {
+ bool: {
+ must_not: {
+ exists: {
+ field: 'job_type',
+ },
+ },
+ },
+ },
+ ],
+ minimum_should_match: 1,
+ },
+};
+
export function jobAuditMessagesProvider(callWithRequest) {
// search for audit messages,
// jobId is optional. without it, all jobs will be listed.
@@ -47,13 +71,9 @@ export function jobAuditMessagesProvider(callWithRequest) {
level: 'activity',
},
},
- must: {
- term: {
- job_type: 'anomaly_detector',
- },
- },
},
},
+ anomalyDetectorTypeFilter,
timeFilter,
],
},
@@ -119,6 +139,7 @@ export function jobAuditMessagesProvider(callWithRequest) {
},
},
},
+ anomalyDetectorTypeFilter,
],
},
};
diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx
index 77d51f68c18ca..05a33eeba1434 100644
--- a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx
@@ -42,6 +42,10 @@ const WrappedByAutoSizer = styled.div`
`; // required by AutoSizer
WrappedByAutoSizer.displayName = 'WrappedByAutoSizer';
+const StyledEuiPanel = styled(EuiPanel)`
+ max-width: 100%;
+`;
+
interface Props {
browserFields: BrowserFields;
columns: ColumnHeader[];
@@ -113,7 +117,7 @@ const EventsViewerComponent: React.FC = ({
);
return (
-
+
{({ measureRef, content: { width = 0 } }) => (
<>
@@ -225,7 +229,7 @@ const EventsViewerComponent: React.FC = ({
>
)}
-
+
);
};
diff --git a/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx b/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx
index db6ff7cf55f92..5a286532fabfc 100644
--- a/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx
@@ -55,7 +55,7 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine
display="condensed"
navTabs={
hideDetectionEngine
- ? pickBy((_, key) => key !== SiemPageName.detectionEngine, navTabs)
+ ? pickBy((_, key) => key !== SiemPageName.detections, navTabs)
: navTabs
}
/>
diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx
index 0519f5c7c956b..3180fc955c690 100644
--- a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx
@@ -61,32 +61,32 @@ export const LinkToPage = React.memo(({ match }) => (
;
-export const DETECTION_ENGINE_PAGE_NAME = 'detection-engine';
+export const DETECTION_ENGINE_PAGE_NAME = 'detections';
export const RedirectToDetectionEnginePage = ({
match: {
diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx
index cae209a76fc1c..b6efc07ad8fe3 100644
--- a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx
@@ -64,12 +64,12 @@ describe('SIEM Navigation', () => {
expect(setBreadcrumbs).toHaveBeenNthCalledWith(1, {
detailName: undefined,
navTabs: {
- 'detection-engine': {
+ detections: {
disabled: false,
- href: '#/link-to/detection-engine',
- id: 'detection-engine',
- name: 'Detection engine',
- urlKey: 'detection-engine',
+ href: '#/link-to/detections',
+ id: 'detections',
+ name: 'Detections',
+ urlKey: 'detections',
},
hosts: {
disabled: false,
@@ -146,12 +146,12 @@ describe('SIEM Navigation', () => {
detailName: undefined,
filters: [],
navTabs: {
- 'detection-engine': {
+ detections: {
disabled: false,
- href: '#/link-to/detection-engine',
- id: 'detection-engine',
- name: 'Detection engine',
- urlKey: 'detection-engine',
+ href: '#/link-to/detections',
+ id: 'detections',
+ name: 'Detections',
+ urlKey: 'detections',
},
hosts: {
disabled: false,
diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx
index c7259edbdc593..009ab141e958e 100644
--- a/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx
@@ -45,11 +45,25 @@ const MyEuiFlexItem = styled(EuiFlexItem)`
white-space: nowrap;
`;
-const EuiSelectableContainer = styled.div`
+const EuiSelectableContainer = styled.div<{ loading: boolean }>`
.euiSelectable {
.euiFormControlLayout__childrenWrapper {
display: flex;
}
+ ${({ loading }) => `${
+ loading
+ ? `
+ .euiFormControlLayoutIcons {
+ display: none;
+ }
+ .euiFormControlLayoutIcons.euiFormControlLayoutIcons--right {
+ display: block;
+ left: 12px;
+ top: 12px;
+ }`
+ : ''
+ }
+ `}
}
`;
@@ -265,7 +279,7 @@ const SearchTimelineSuperSelectComponent: React.FC
{({ timelines, loading, totalCount }) => (
-
+
{
- describe('isKqlForRoute', () => {
- test('host page and host page kuery', () => {
- const result = isKqlForRoute(SiemPageName.hosts, undefined, CONSTANTS.hostsPage);
- expect(result).toBeTruthy();
- });
- test('host page and host details kuery', () => {
- const result = isKqlForRoute(SiemPageName.hosts, undefined, CONSTANTS.hostsDetails);
- expect(result).toBeFalsy();
- });
- test('host details and host details kuery', () => {
- const result = isKqlForRoute(SiemPageName.hosts, 'siem-kibana', CONSTANTS.hostsDetails);
- expect(result).toBeTruthy();
- });
- test('host details and host page kuery', () => {
- const result = isKqlForRoute(SiemPageName.hosts, 'siem-kibana', CONSTANTS.hostsPage);
- expect(result).toBeFalsy();
- });
- test('network page and network page kuery', () => {
- const result = isKqlForRoute(SiemPageName.network, undefined, CONSTANTS.networkPage);
- expect(result).toBeTruthy();
- });
- test('network page and network details kuery', () => {
- const result = isKqlForRoute(SiemPageName.network, undefined, CONSTANTS.networkDetails);
- expect(result).toBeFalsy();
- });
- test('network details and network details kuery', () => {
- const result = isKqlForRoute(SiemPageName.network, '10.100.7.198', CONSTANTS.networkDetails);
- expect(result).toBeTruthy();
- });
- test('network details and network page kuery', () => {
- const result = isKqlForRoute(SiemPageName.network, '123.234.34', CONSTANTS.networkPage);
- expect(result).toBeFalsy();
- });
- });
describe('getTitle', () => {
test('host page name', () => {
const result = getTitle('hosts', undefined, navTabs);
diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts
index aa340b54c1699..6ba5810f794b0 100644
--- a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts
+++ b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts
@@ -78,8 +78,8 @@ export const getUrlType = (pageName: string): UrlStateType => {
return 'host';
} else if (pageName === SiemPageName.network) {
return 'network';
- } else if (pageName === SiemPageName.detectionEngine) {
- return 'detection-engine';
+ } else if (pageName === SiemPageName.detections) {
+ return 'detections';
} else if (pageName === SiemPageName.timelines) {
return 'timeline';
}
@@ -111,31 +111,14 @@ export const getCurrentLocation = (
return CONSTANTS.networkDetails;
}
return CONSTANTS.networkPage;
- } else if (pageName === SiemPageName.detectionEngine) {
- return CONSTANTS.detectionEnginePage;
+ } else if (pageName === SiemPageName.detections) {
+ return CONSTANTS.detectionsPage;
} else if (pageName === SiemPageName.timelines) {
return CONSTANTS.timelinePage;
}
return CONSTANTS.unknown;
};
-export const isKqlForRoute = (
- pageName: string,
- detailName: string | undefined,
- queryLocation: LocationTypes | null = null
-): boolean => {
- const currentLocation = getCurrentLocation(pageName, detailName);
- if (
- (currentLocation === CONSTANTS.hostsPage && queryLocation === CONSTANTS.hostsPage) ||
- (currentLocation === CONSTANTS.networkPage && queryLocation === CONSTANTS.networkPage) ||
- (currentLocation === CONSTANTS.hostsDetails && queryLocation === CONSTANTS.hostsDetails) ||
- (currentLocation === CONSTANTS.networkDetails && queryLocation === CONSTANTS.networkDetails)
- ) {
- return true;
- }
- return false;
-};
-
export const makeMapStateToProps = () => {
const getInputsSelector = inputsSelectors.inputsSelector();
const getGlobalQuerySelector = inputsSelectors.globalQuerySelector();
diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts
index a48653a7ea6f4..be1ae1ad63bd4 100644
--- a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts
+++ b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts
@@ -24,7 +24,7 @@ export const ALL_URL_STATE_KEYS: KeyUrlState[] = [
];
export const URL_STATE_KEYS: Record = {
- 'detection-engine': [
+ detections: [
CONSTANTS.appQuery,
CONSTANTS.filters,
CONSTANTS.savedQuery,
@@ -56,7 +56,7 @@ export const URL_STATE_KEYS: Record = {
};
export type LocationTypes =
- | CONSTANTS.detectionEnginePage
+ | CONSTANTS.detectionsPage
| CONSTANTS.hostsDetails
| CONSTANTS.hostsPage
| CONSTANTS.networkDetails
diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts
index 8cd3e8f2d45c7..a83e874437c10 100644
--- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts
+++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts
@@ -322,7 +322,7 @@ export const getRuleStatusById = async ({
}: {
id: string;
signal: AbortSignal;
-}): Promise> => {
+}): Promise> => {
const response = await fetch(
`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_STATUS}?ids=${encodeURIComponent(
JSON.stringify([id])
diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/index.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/index.ts
index a61cbabd80626..e9a0f27b34696 100644
--- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/index.ts
+++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/index.ts
@@ -10,3 +10,4 @@ export * from './persist_rule';
export * from './types';
export * from './use_rule';
export * from './use_rules';
+export * from './use_rule_status';
diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts
index 334daa8d1d028..0dcd0da5be8f6 100644
--- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts
+++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts
@@ -181,9 +181,15 @@ export interface ExportRulesProps {
}
export interface RuleStatus {
+ current_status: RuleInfoStatus;
+ failures: RuleInfoStatus[];
+}
+
+export type RuleStatusType = 'executing' | 'failed' | 'going to run' | 'succeeded';
+export interface RuleInfoStatus {
alert_id: string;
status_date: string;
- status: string;
+ status: RuleStatusType | null;
last_failure_at: string | null;
last_success_at: string | null;
last_failure_message: string | null;
diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_create_packaged_rules.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_create_packaged_rules.tsx
new file mode 100644
index 0000000000000..592419f879011
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_create_packaged_rules.tsx
@@ -0,0 +1,81 @@
+/*
+ * 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 { useEffect, useState } from 'react';
+
+import { createPrepackagedRules } from './api';
+
+type Return = [boolean, boolean | null];
+
+interface UseCreatePackagedRules {
+ canUserCRUD: boolean | null;
+ hasIndexManage: boolean | null;
+ hasManageApiKey: boolean | null;
+ isAuthenticated: boolean | null;
+ isSignalIndexExists: boolean | null;
+}
+
+/**
+ * Hook for creating the packages rules
+ *
+ * @param canUserCRUD boolean
+ * @param hasIndexManage boolean
+ * @param hasManageApiKey boolean
+ * @param isAuthenticated boolean
+ * @param isSignalIndexExists boolean
+ *
+ * @returns [loading, hasCreatedPackageRules]
+ */
+export const useCreatePackagedRules = ({
+ canUserCRUD,
+ hasIndexManage,
+ hasManageApiKey,
+ isAuthenticated,
+ isSignalIndexExists,
+}: UseCreatePackagedRules): Return => {
+ const [hasCreatedPackageRules, setHasCreatedPackageRules] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ let isSubscribed = true;
+ const abortCtrl = new AbortController();
+ setLoading(true);
+
+ async function createRules() {
+ try {
+ await createPrepackagedRules({
+ signal: abortCtrl.signal,
+ });
+
+ if (isSubscribed) {
+ setHasCreatedPackageRules(true);
+ }
+ } catch (error) {
+ if (isSubscribed) {
+ setHasCreatedPackageRules(false);
+ }
+ }
+ if (isSubscribed) {
+ setLoading(false);
+ }
+ }
+ if (
+ canUserCRUD &&
+ hasIndexManage &&
+ hasManageApiKey &&
+ isAuthenticated &&
+ isSignalIndexExists
+ ) {
+ createRules();
+ }
+ return () => {
+ isSubscribed = false;
+ abortCtrl.abort();
+ };
+ }, [canUserCRUD, hasIndexManage, hasManageApiKey, isAuthenticated, isSignalIndexExists]);
+
+ return [loading, hasCreatedPackageRules];
+};
diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx
index 216fbcea861a3..466c2cddac97d 100644
--- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx
+++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
import { useStateToaster } from '../../../components/toasters';
import { errorToToaster } from '../../../components/ml/api/error_to_toaster';
@@ -12,7 +12,8 @@ import { getRuleStatusById } from './api';
import * as i18n from './translations';
import { RuleStatus } from './types';
-type Return = [boolean, RuleStatus[] | null];
+type Func = (ruleId: string) => void;
+type Return = [boolean, RuleStatus | null, Func | null];
/**
* Hook for using to get a Rule from the Detection Engine API
@@ -21,7 +22,8 @@ type Return = [boolean, RuleStatus[] | null];
*
*/
export const useRuleStatus = (id: string | undefined | null): Return => {
- const [ruleStatus, setRuleStatus] = useState(null);
+ const [ruleStatus, setRuleStatus] = useState(null);
+ const fetchRuleStatus = useRef(null);
const [loading, setLoading] = useState(true);
const [, dispatchToaster] = useStateToaster();
@@ -29,7 +31,7 @@ export const useRuleStatus = (id: string | undefined | null): Return => {
let isSubscribed = true;
const abortCtrl = new AbortController();
- async function fetchData(idToFetch: string) {
+ const fetchData = async (idToFetch: string) => {
try {
setLoading(true);
const ruleStatusResponse = await getRuleStatusById({
@@ -49,15 +51,16 @@ export const useRuleStatus = (id: string | undefined | null): Return => {
if (isSubscribed) {
setLoading(false);
}
- }
+ };
if (id != null) {
fetchData(id);
}
+ fetchRuleStatus.current = fetchData;
return () => {
isSubscribed = false;
abortCtrl.abort();
};
}, [id]);
- return [loading, ruleStatus];
+ return [loading, ruleStatus, fetchRuleStatus.current];
};
diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts
index 34cb7684a0399..ea4860dafd40f 100644
--- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts
+++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts
@@ -96,5 +96,5 @@ export interface Privilege {
write: boolean;
};
};
- isAuthenticated: boolean;
+ is_authenticated: boolean;
}
diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx
index 792ff29ad2488..7d0e331200d55 100644
--- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx
+++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx
@@ -42,7 +42,7 @@ export const usePrivilegeUser = (): Return => {
});
if (isSubscribed && privilege != null) {
- setAuthenticated(privilege.isAuthenticated);
+ setAuthenticated(privilege.is_authenticated);
if (privilege.index != null && Object.keys(privilege.index).length > 0) {
const indexName = Object.keys(privilege.index)[0];
setHasIndexManage(privilege.index[indexName].manage);
diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx
index 189d8a1bf3f75..c1ee5fd12b8c1 100644
--- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx
+++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx
@@ -8,7 +8,6 @@ import { useEffect, useState, useRef } from 'react';
import { errorToToaster } from '../../../components/ml/api/error_to_toaster';
import { useStateToaster } from '../../../components/toasters';
-import { createPrepackagedRules } from '../rules';
import { createSignalIndex, getSignalIndex } from './api';
import * as i18n from './translations';
import { PostSignalError, SignalIndexError } from './types';
@@ -41,7 +40,6 @@ export const useSignalIndex = (): Return => {
if (isSubscribed && signal != null) {
setSignalIndexName(signal.name);
setSignalIndexExists(true);
- createPrepackagedRules({ signal: abortCtrl.signal });
}
} catch (error) {
if (isSubscribed) {
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx
index 8290da1ba3220..5f017a3a1f67f 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx
@@ -45,7 +45,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 1,
rule: {
- href: '#/detection-engine/rules/rule-details',
+ href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@@ -55,7 +55,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 2,
rule: {
- href: '#/detection-engine/rules/rule-details',
+ href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@@ -65,7 +65,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 3,
rule: {
- href: '#/detection-engine/rules/rule-details',
+ href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@@ -76,7 +76,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 4,
rule: {
- href: '#/detection-engine/rules/rule-details',
+ href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@@ -87,7 +87,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 5,
rule: {
- href: '#/detection-engine/rules/rule-details',
+ href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@@ -98,7 +98,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 6,
rule: {
- href: '#/detection-engine/rules/rule-details',
+ href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@@ -109,7 +109,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 7,
rule: {
- href: '#/detection-engine/rules/rule-details',
+ href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@@ -120,7 +120,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 8,
rule: {
- href: '#/detection-engine/rules/rule-details',
+ href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@@ -131,7 +131,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 9,
rule: {
- href: '#/detection-engine/rules/rule-details',
+ href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@@ -142,7 +142,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 10,
rule: {
- href: '#/detection-engine/rules/rule-details',
+ href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@@ -153,7 +153,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 11,
rule: {
- href: '#/detection-engine/rules/rule-details',
+ href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@@ -164,7 +164,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 12,
rule: {
- href: '#/detection-engine/rules/rule-details',
+ href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@@ -175,7 +175,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 13,
rule: {
- href: '#/detection-engine/rules/rule-details',
+ href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@@ -186,7 +186,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 14,
rule: {
- href: '#/detection-engine/rules/rule-details',
+ href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@@ -197,7 +197,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 15,
rule: {
- href: '#/detection-engine/rules/rule-details',
+ href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@@ -208,7 +208,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 16,
rule: {
- href: '#/detection-engine/rules/rule-details',
+ href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@@ -219,7 +219,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 17,
rule: {
- href: '#/detection-engine/rules/rule-details',
+ href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@@ -230,7 +230,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 18,
rule: {
- href: '#/detection-engine/rules/rule-details',
+ href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@@ -241,7 +241,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 19,
rule: {
- href: '#/detection-engine/rules/rule-details',
+ href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@@ -252,7 +252,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 20,
rule: {
- href: '#/detection-engine/rules/rule-details',
+ href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
@@ -263,7 +263,7 @@ export const ActivityMonitor = React.memo(() => {
{
id: 21,
rule: {
- href: '#/detection-engine/rules/rule-details',
+ href: '#/detections/rules/rule-details',
name: 'Automated exfiltration',
},
ran: '2019-12-28 00:00:00.000-05:00',
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx
index 5c4795a819275..e00dfa5b84473 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx
@@ -188,13 +188,13 @@ export const getSignalsActions = ({
updateTimelineIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => void;
}): TimelineAction[] => [
{
- getAction: ({ eventId, ecsData }: TimelineActionProps): JSX.Element => (
+ getAction: ({ ecsData }: TimelineActionProps): JSX.Element => (
sendSignalToTimelineAction({
apolloClient,
@@ -203,7 +203,7 @@ export const getSignalsActions = ({
updateTimelineIsLoading,
})
}
- iconType="tableDensityNormal"
+ iconType="timeline"
aria-label="Next"
/>
@@ -228,7 +228,7 @@ export const getSignalsActions = ({
})
}
isDisabled={!canUserCRUD || !hasIndexWrite}
- iconType={status === FILTER_OPEN ? 'indexOpen' : 'indexClose'}
+ iconType={status === FILTER_OPEN ? 'securitySignalDetected' : 'securitySignalResolved'}
aria-label="Next"
/>
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/batch_actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/batch_actions.tsx
index b756b2eb75a7a..bb45ff68cb01d 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/batch_actions.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/batch_actions.tsx
@@ -61,7 +61,7 @@ export const getBatchItems = ({
{
closePopover();
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx
index 0af3635d4c473..13d77385c53d4 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx
@@ -80,7 +80,7 @@ const SignalsUtilityBarComponent: React.FC = ({
{isFilteredToOpen
@@ -89,7 +89,7 @@ const SignalsUtilityBarComponent: React.FC = ({
{
if (!showClearSelection) {
selectAll();
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts
index 0245b9968cc36..8c88fa4a5dae6 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts
@@ -86,7 +86,7 @@ export const STACK_BY_USERS = i18n.translate(
export const HISTOGRAM_HEADER = i18n.translate(
'xpack.siem.detectionEngine.signals.histogram.headerTitle',
{
- defaultMessage: 'Signal detection frequency',
+ defaultMessage: 'Signal count',
}
);
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx
index bbaccb7882484..24e14473d40e9 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx
@@ -10,6 +10,7 @@ import React, { useEffect, useReducer, Dispatch, createContext, useContext } fro
import { usePrivilegeUser } from '../../../../containers/detection_engine/signals/use_privilege_user';
import { useSignalIndex } from '../../../../containers/detection_engine/signals/use_signal_index';
import { useKibana } from '../../../../lib/kibana';
+import { useCreatePackagedRules } from '../../../../containers/detection_engine/rules/use_create_packaged_rules';
export interface State {
canUserCRUD: boolean | null;
@@ -161,6 +162,14 @@ export const useUserInfo = (): State => {
createSignalIndex,
] = useSignalIndex();
+ useCreatePackagedRules({
+ canUserCRUD,
+ hasIndexManage,
+ hasManageApiKey,
+ isAuthenticated,
+ isSignalIndexExists,
+ });
+
const uiCapabilities = useKibana().services.application.capabilities;
const capabilitiesCanUserCRUD: boolean =
typeof uiCapabilities.siem.crud === 'boolean' ? uiCapabilities.siem.crud : false;
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx
index 26a9ad128b1dc..d9e0377b34060 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx
@@ -148,7 +148,7 @@ const DetectionEngineComponent = React.memo(
}
title={i18n.PAGE_TITLE}
>
-
+
{i18n.BUTTON_MANAGE_RULES}
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx
index 7a0b8df85416c..33186d2787d8a 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx
@@ -15,7 +15,7 @@ import { RuleDetails } from './rules/details';
import { RulesComponent } from './rules';
import { DetectionEngineTab } from './types';
-const detectionEnginePath = `/:pageName(detection-engine)`;
+const detectionEnginePath = `/:pageName(detections)`;
type Props = Partial> & { url: string };
@@ -42,12 +42,9 @@ export const DetectionEngineContainer = React.memo(() => (
(
-
+
)}
/>
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts
index 757c1fabfc9cd..b79b3ed091f16 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts
@@ -60,7 +60,7 @@ export const mockTableData: TableData[] = [
lastResponse: { type: '—' },
method: 'saved_query',
rule: {
- href: '#/detection-engine/rules/id/abe6c564-050d-45a5-aaf0-386c37dd1f61',
+ href: '#/detections/rules/id/abe6c564-050d-45a5-aaf0-386c37dd1f61',
name: 'Home Grown!',
status: 'Status Placeholder',
},
@@ -112,7 +112,7 @@ export const mockTableData: TableData[] = [
lastResponse: { type: '—' },
method: 'saved_query',
rule: {
- href: '#/detection-engine/rules/id/63f06f34-c181-4b2d-af35-f2ace572a1ee',
+ href: '#/detections/rules/id/63f06f34-c181-4b2d-af35-f2ace572a1ee',
name: 'Home Grown!',
status: 'Status Placeholder',
},
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx
index a73e656dddd96..d546c4edb55d3 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx
@@ -30,6 +30,7 @@ import { FormattedDate } from '../../../../components/formatted_date';
import { RuleSwitch } from '../components/rule_switch';
import { SeverityBadge } from '../components/severity_badge';
import { ActionToaster } from '../../../../components/toasters';
+import { getStatusColor } from '../components/rule_status/helpers';
const getActions = (
dispatch: React.Dispatch,
@@ -86,7 +87,7 @@ export const getColumns = (
field: 'method',
name: i18n.COLUMN_METHOD,
truncateText: true,
- width: '16%',
+ width: '14%',
},
{
field: 'severity',
@@ -113,19 +114,11 @@ export const getColumns = (
field: 'status',
name: i18n.COLUMN_LAST_RESPONSE,
render: (value: TableData['status']) => {
- const color =
- value == null
- ? 'subdued'
- : value === 'succeeded'
- ? 'success'
- : value === 'failed'
- ? 'danger'
- : value === 'executing'
- ? 'warning'
- : 'subdued';
return (
<>
- {value ?? getEmptyTagValue()}
+
+ {value ?? getEmptyTagValue()}
+
>
);
},
@@ -161,7 +154,7 @@ export const getColumns = (
/>
),
sortable: true,
- width: '85px',
+ width: '95px',
},
];
const actions: RulesColumns[] = [
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts
index 9666b7a5688cf..07a2f2f278987 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts
@@ -24,7 +24,7 @@ export const formatRules = (rules: Rule[], selectedIds?: string[]): TableData[]
immutable: rule.immutable,
rule_id: rule.rule_id,
rule: {
- href: `#/detection-engine/rules/id/${encodeURIComponent(rule.id)}`,
+ href: `#/detections/rules/id/${encodeURIComponent(rule.id)}`,
name: rule.name,
status: 'Status Placeholder',
},
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx
index e8b6919165c8b..011c008c5b2d2 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx
@@ -125,7 +125,7 @@ export const buildThreatsDescription = ({
description: (
{threats.map((threat, index) => {
- const tactic = tacticsOptions.find(t => t.name === threat.tactic.name);
+ const tactic = tacticsOptions.find(t => t.id === threat.tactic.id);
return (
@@ -133,7 +133,7 @@ export const buildThreatsDescription = ({
{threat.techniques.map(technique => {
- const myTechnique = techniquesOptions.find(t => t.name === technique.name);
+ const myTechnique = techniquesOptions.find(t => t.id === technique.id);
return (
- theme.euiColorPrimary};
+ width: 40px;
+ height: 40px;
+ }
+`;
+
interface RuleActionsOverflowComponentProps {
rule: Rule | null;
userHasNoPermissions: boolean;
@@ -86,20 +98,29 @@ const RuleActionsOverflowComponent = ({
[rule, userHasNoPermissions]
);
+ const handlePopoverOpen = useCallback(() => {
+ setIsPopoverOpen(!isPopoverOpen);
+ }, [setIsPopoverOpen, isPopoverOpen]);
+
+ const button = useMemo(
+ () => (
+
+
+
+ ),
+ [handlePopoverOpen, userHasNoPermissions]
+ );
+
return (
<>
- setIsPopoverOpen(!isPopoverOpen)}
- />
-
- }
+ button={button}
closePopover={() => setIsPopoverOpen(false)}
id="ruleActionsOverflow"
isOpen={isPopoverOpen}
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.ts
new file mode 100644
index 0000000000000..263f602251ea7
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.ts
@@ -0,0 +1,18 @@
+/*
+ * 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 { RuleStatusType } from '../../../../../containers/detection_engine/rules';
+
+export const getStatusColor = (status: RuleStatusType | string | null) =>
+ status == null
+ ? 'subdued'
+ : status === 'succeeded'
+ ? 'success'
+ : status === 'failed'
+ ? 'danger'
+ : status === 'executing' || status === 'going to run'
+ ? 'warning'
+ : 'subdued';
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx
new file mode 100644
index 0000000000000..2c9173cbeb694
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx
@@ -0,0 +1,99 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ EuiButtonIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiHealth,
+ EuiLoadingSpinner,
+ EuiText,
+} from '@elastic/eui';
+import { isEqual } from 'lodash/fp';
+import React, { memo, useCallback, useEffect, useState } from 'react';
+
+import { useRuleStatus, RuleInfoStatus } from '../../../../../containers/detection_engine/rules';
+import { FormattedDate } from '../../../../../components/formatted_date';
+import { getEmptyTagValue } from '../../../../../components/empty_value';
+import { getStatusColor } from './helpers';
+import * as i18n from './translations';
+
+interface RuleStatusProps {
+ ruleId: string | null;
+ ruleEnabled?: boolean | null;
+}
+
+const RuleStatusComponent: React.FC = ({ ruleId, ruleEnabled }) => {
+ const [loading, ruleStatus, fetchRuleStatus] = useRuleStatus(ruleId);
+ const [myRuleEnabled, setMyRuleEnabled] = useState(ruleEnabled ?? null);
+ const [currentStatus, setCurrentStatus] = useState(
+ ruleStatus?.current_status ?? null
+ );
+
+ useEffect(() => {
+ if (myRuleEnabled !== ruleEnabled && fetchRuleStatus != null && ruleId != null) {
+ fetchRuleStatus(ruleId);
+ if (myRuleEnabled !== ruleEnabled) {
+ setMyRuleEnabled(ruleEnabled ?? null);
+ }
+ }
+ }, [fetchRuleStatus, myRuleEnabled, ruleId, ruleEnabled, setMyRuleEnabled]);
+
+ useEffect(() => {
+ if (!isEqual(currentStatus, ruleStatus?.current_status)) {
+ setCurrentStatus(ruleStatus?.current_status ?? null);
+ }
+ }, [currentStatus, ruleStatus, setCurrentStatus]);
+
+ const handleRefresh = useCallback(() => {
+ if (fetchRuleStatus != null && ruleId != null) {
+ fetchRuleStatus(ruleId);
+ }
+ }, [fetchRuleStatus, ruleId]);
+
+ return (
+
+
+ {i18n.STATUS}
+ {':'}
+
+ {loading && (
+
+
+
+ )}
+ {!loading && (
+ <>
+
+
+ {currentStatus?.status ?? getEmptyTagValue()}
+
+
+ {currentStatus?.status_date != null && currentStatus?.status != null && (
+ <>
+
+ <>{i18n.STATUS_AT}>
+
+
+
+
+ >
+ )}
+
+
+
+ >
+ )}
+
+ );
+};
+
+export const RuleStatus = memo(RuleStatusComponent);
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/translations.ts
new file mode 100644
index 0000000000000..e03cc252ad729
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/translations.ts
@@ -0,0 +1,29 @@
+/*
+ * 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';
+
+export const STATUS = i18n.translate('xpack.siem.detectionEngine.ruleStatus.statusDescription', {
+ defaultMessage: 'Status',
+});
+
+export const STATUS_AT = i18n.translate(
+ 'xpack.siem.detectionEngine.ruleStatus.statusAtDescription',
+ {
+ defaultMessage: 'at',
+ }
+);
+
+export const STATUS_DATE = i18n.translate(
+ 'xpack.siem.detectionEngine.ruleStatus.statusDateDescription',
+ {
+ defaultMessage: 'Status date',
+ }
+);
+
+export const REFRESH = i18n.translate('xpack.siem.detectionEngine.ruleStatus.refreshButton', {
+ defaultMessage: 'Refresh',
+});
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx
index 9cb0323ed8987..09b7ecc9df982 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx
@@ -36,6 +36,7 @@ export interface RuleSwitchProps {
isDisabled?: boolean;
isLoading?: boolean;
optionLabel?: string;
+ onChange?: (enabled: boolean) => void;
}
/**
@@ -48,6 +49,7 @@ export const RuleSwitchComponent = ({
isLoading,
enabled,
optionLabel,
+ onChange,
}: RuleSwitchProps) => {
const [myIsLoading, setMyIsLoading] = useState(false);
const [myEnabled, setMyEnabled] = useState(enabled ?? false);
@@ -65,6 +67,9 @@ export const RuleSwitchComponent = ({
enabled: event.target.checked!,
});
setMyEnabled(updatedRules[0].enabled);
+ if (onChange != null) {
+ onChange(updatedRules[0].enabled);
+ }
} catch {
setMyIsLoading(false);
}
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx
index 0ef104e6891df..3bde2087f26b1 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx
@@ -150,7 +150,13 @@ export const ScheduleItem = ({
/>
}
>
-
+
);
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts
index 328c4a0f96066..92aca1cecf9f3 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts
@@ -5,6 +5,7 @@
*/
import { AboutStepRule } from '../../types';
+import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/search_super_select/translations';
export const threatsDefault = [
{
@@ -25,7 +26,7 @@ export const stepAboutDefaultValue: AboutStepRule = {
tags: [],
timeline: {
id: null,
- title: null,
+ title: DEFAULT_TIMELINE_TITLE,
},
threats: threatsDefault,
};
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx
index 0e03a11776fb7..73c07673a82f4 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx
@@ -5,10 +5,11 @@
*/
import { EuiButton, EuiHorizontalRule, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import { isEqual, get } from 'lodash/fp';
+import { isEqual } from 'lodash/fp';
import React, { FC, memo, useCallback, useEffect, useState } from 'react';
import styled from 'styled-components';
+import { setFieldValue } from '../../helpers';
import { RuleStepProps, RuleStep, AboutStepRule } from '../../types';
import * as RuleI18n from '../../translations';
import { AddItem } from '../add_item_form';
@@ -71,14 +72,7 @@ const StepAboutRuleComponent: FC = ({
isNew: false,
};
setMyStepData(myDefaultValues);
- if (!isReadOnlyView) {
- Object.keys(schema).forEach(key => {
- const val = get(key, myDefaultValues);
- if (val != null) {
- form.setFieldValue(key, val);
- }
- });
- }
+ setFieldValue(form, schema, myDefaultValues);
}
}, [defaultValues]);
@@ -88,7 +82,7 @@ const StepAboutRuleComponent: FC = ({
}
}, [form]);
- return isReadOnlyView && myStepData != null ? (
+ return isReadOnlyView && myStepData.name != null ? (
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx
index 6bdef4a69af1e..5409a5f161bba 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx
@@ -11,7 +11,7 @@ import {
EuiFlexItem,
EuiButton,
} from '@elastic/eui';
-import { isEmpty, isEqual, get } from 'lodash/fp';
+import { isEmpty, isEqual } from 'lodash/fp';
import React, { FC, memo, useCallback, useState, useEffect } from 'react';
import styled from 'styled-components';
@@ -19,6 +19,7 @@ import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/pu
import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules';
import { DEFAULT_INDEX_KEY } from '../../../../../../common/constants';
import { useUiSetting$ } from '../../../../../lib/kibana';
+import { setFieldValue } from '../../helpers';
import * as RuleI18n from '../../translations';
import { DefineStepRule, RuleStep, RuleStepProps } from '../../types';
import { StepRuleDescription } from '../description_step';
@@ -121,14 +122,7 @@ const StepDefineRuleComponent: FC = ({
if (!isEqual(myDefaultValues, myStepData)) {
setMyStepData(myDefaultValues);
setLocalUseIndicesConfig(isEqual(myDefaultValues.index, indicesConfig));
- if (!isReadOnlyView) {
- Object.keys(schema).forEach(key => {
- const val = get(key, myDefaultValues);
- if (val != null) {
- form.setFieldValue(key, val);
- }
- });
- }
+ setFieldValue(form, schema, myDefaultValues);
}
}
}, [defaultValues, indicesConfig]);
@@ -152,7 +146,7 @@ const StepDefineRuleComponent: FC = ({
setOpenTimelineSearch(false);
}, []);
- return isReadOnlyView && myStepData != null ? (
+ return isReadOnlyView && myStepData?.queryBar != null ? (
= ({
@@ -67,14 +68,7 @@ const StepScheduleRuleComponent: FC = ({
isNew: false,
};
setMyStepData(myDefaultValues);
- if (!isReadOnlyView) {
- Object.keys(schema).forEach(key => {
- const val = get(key, myDefaultValues);
- if (val != null) {
- form.setFieldValue(key, val);
- }
- });
- }
+ setFieldValue(form, schema, myDefaultValues);
}
}, [defaultValues]);
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx
index 4da17b88b9ad0..a951c1fab7cc8 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx
@@ -14,13 +14,14 @@ export const schema: FormSchema = {
label: i18n.translate(
'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldIntervalLabel',
{
- defaultMessage: 'Rule run interval & look-back',
+ defaultMessage: 'Runs every',
}
),
helpText: i18n.translate(
'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldIntervalHelpText',
{
- defaultMessage: 'How often and how far back this rule will search specified indices.',
+ defaultMessage:
+ 'Rules run periodically and detect signals within the specified time frame.',
}
),
},
@@ -28,15 +29,14 @@ export const schema: FormSchema = {
label: i18n.translate(
'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldAdditionalLookBackLabel',
{
- defaultMessage: 'Additional look-back',
+ defaultMessage: 'Additional look-back time',
}
),
labelAppend: OptionalFieldLabel,
helpText: i18n.translate(
'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldAdditionalLookBackHelpText',
{
- defaultMessage:
- 'Add more time to the look-back range in order to prevent potential gaps in signal reporting.',
+ defaultMessage: 'Adds time to the look-back period to prevent missed signals.',
}
),
},
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/translations.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/translations.tsx
index feaaf4e85b2af..67bcc1af8150b 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/translations.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/translations.tsx
@@ -9,13 +9,13 @@ import { i18n } from '@kbn/i18n';
export const COMPLETE_WITHOUT_ACTIVATING = i18n.translate(
'xpack.siem.detectionEngine.createRule. stepScheduleRule.completeWithoutActivatingTitle',
{
- defaultMessage: 'Complete rule without activating',
+ defaultMessage: 'Create rule without activating it',
}
);
export const COMPLETE_WITH_ACTIVATING = i18n.translate(
'xpack.siem.detectionEngine.createRule. stepScheduleRule.completeWithActivatingTitle',
{
- defaultMessage: 'Complete rule & activate',
+ defaultMessage: 'Create & activate rule',
}
);
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx
index e5656f5b081fb..cbc60015d9c87 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx
@@ -9,10 +9,11 @@ import React, { useCallback, useRef, useState } from 'react';
import { Redirect } from 'react-router-dom';
import styled from 'styled-components';
+import { usePersistRule } from '../../../../containers/detection_engine/rules';
import { HeaderPage } from '../../../../components/header_page';
import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine';
import { WrapperPage } from '../../../../components/wrapper_page';
-import { usePersistRule } from '../../../../containers/detection_engine/rules';
+import { displaySuccessToast, useStateToaster } from '../../../../components/toasters';
import { SpyRoute } from '../../../../utils/route/spy_routes';
import { useUserInfo } from '../../components/user_info';
import { AccordionTitle } from '../components/accordion_title';
@@ -55,6 +56,7 @@ export const CreateRuleComponent = React.memo(() => {
canUserCRUD,
hasManageApiKey,
} = useUserInfo();
+ const [, dispatchToaster] = useStateToaster();
const [openAccordionId, setOpenAccordionId] = useState(RuleStep.defineRule);
const defineRuleRef = useRef(null);
const aboutRuleRef = useRef(null);
@@ -95,6 +97,7 @@ export const CreateRuleComponent = React.memo(() => {
const stepRuleIdx = stepsRuleOrder.findIndex(item => step === item);
if ([0, 1].includes(stepRuleIdx)) {
if (isStepRuleInReadOnlyView[stepsRuleOrder[stepRuleIdx + 1]]) {
+ setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]);
setIsStepRuleInEditView({
...isStepRuleInReadOnlyView,
[step]: true,
@@ -203,12 +206,15 @@ export const CreateRuleComponent = React.memo(() => {
async (id: RuleStep) => {
const activeForm = await stepsForm.current[openAccordionId]?.submit();
if (activeForm != null && activeForm?.isValid) {
+ stepsData.current[openAccordionId] = {
+ ...stepsData.current[openAccordionId],
+ data: activeForm.data,
+ isValid: activeForm.isValid,
+ };
setOpenAccordionId(id);
- openCloseAccordion(openAccordionId);
-
setIsStepRuleInEditView({
...isStepRuleInReadOnlyView,
- [openAccordionId]: openAccordionId === RuleStep.scheduleRule ? false : true,
+ [openAccordionId]: true,
[id]: false,
});
}
@@ -217,6 +223,8 @@ export const CreateRuleComponent = React.memo(() => {
);
if (isSaved) {
+ const ruleName = (stepsData.current[RuleStep.aboutRule].data as AboutStepRule).name;
+ displaySuccessToast(i18n.SUCCESSFULLY_CREATED_RULES(ruleName), dispatchToaster);
return ;
}
@@ -224,7 +232,7 @@ export const CreateRuleComponent = React.memo(() => {
<>
{
{
{
+ i18n.translate('xpack.siem.detectionEngine.rules.create.successfullyCreatedRuleTitle', {
+ values: { ruleName },
+ defaultMessage: '{ruleName} was created',
+ });
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx
index 3b49cd30c9aab..f660c1763d5e0 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx
@@ -15,8 +15,7 @@ import {
} from '@elastic/eui';
import React, { memo } from 'react';
-import { useRuleStatus } from '../../../../containers/detection_engine/rules/use_rule_status';
-import { RuleStatus } from '../../../../containers/detection_engine/rules';
+import { useRuleStatus, RuleInfoStatus } from '../../../../containers/detection_engine/rules';
import { HeaderSection } from '../../../../components/header_section';
import * as i18n from './translations';
import { FormattedDate } from '../../../../components/formatted_date';
@@ -35,7 +34,7 @@ const FailureHistoryComponent: React.FC = ({ id }) => {
);
}
- const columns: Array> = [
+ const columns: Array> = [
{
name: i18n.COLUMN_STATUS_TYPE,
render: () => {i18n.TYPE_FAILED} ,
@@ -65,7 +64,9 @@ const FailureHistoryComponent: React.FC = ({ id }) => {
rs.last_failure_at != null) : []}
+ items={
+ ruleStatus != null ? ruleStatus?.failures.filter(rs => rs.last_failure_at != null) : []
+ }
sorting={{ sort: { field: 'status_date', direction: 'desc' } }}
/>
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx
index 86d7178e73c60..a23c681a5aab2 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx
@@ -10,7 +10,6 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
- EuiHealth,
EuiTab,
EuiTabs,
} from '@elastic/eui';
@@ -61,10 +60,10 @@ import { inputsSelectors } from '../../../../store/inputs';
import { State } from '../../../../store';
import { InputsRange } from '../../../../store/inputs/model';
import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../../../store/inputs/actions';
-import { getEmptyTagValue } from '../../../../components/empty_value';
+import { RuleActionsOverflow } from '../components/rule_actions_overflow';
import { RuleStatusFailedCallOut } from './status_failed_callout';
import { FailureHistory } from './failure_history';
-import { RuleActionsOverflow } from '../components/rule_actions_overflow';
+import { RuleStatus } from '../components/rule_status';
interface ReduxProps {
filters: esFilters.Filter[];
@@ -112,6 +111,8 @@ const RuleDetailsComponent = memo(
} = useUserInfo();
const { ruleId } = useParams();
const [isLoading, rule] = useRule(ruleId);
+ // This is used to re-trigger api rule status when user de/activate rule
+ const [ruleEnabled, setRuleEnabled] = useState(null);
const [ruleDetailTab, setRuleDetailTab] = useState(RuleDetailTabs.signals);
const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({
rule,
@@ -181,17 +182,6 @@ const RuleDetailsComponent = memo(
filters,
]);
- const statusColor =
- rule?.status == null
- ? 'subdued'
- : rule?.status === 'succeeded'
- ? 'success'
- : rule?.status === 'failed'
- ? 'danger'
- : rule?.status === 'executing'
- ? 'warning'
- : 'subdued';
-
const tabs = useMemo(
() => (
@@ -229,6 +219,15 @@ const RuleDetailsComponent = memo(
[setAbsoluteRangeDatePicker]
);
+ const handleOnChangeEnabledRule = useCallback(
+ (enabled: boolean) => {
+ if (ruleEnabled == null || enabled !== ruleEnabled) {
+ setRuleEnabled(enabled);
+ }
+ },
+ [ruleEnabled, setRuleEnabled]
+ );
+
return (
<>
{hasIndexWrite != null && !hasIndexWrite && }
@@ -249,7 +248,6 @@ const RuleDetailsComponent = memo(
href: `#${DETECTION_ENGINE_PAGE_NAME}/rules`,
text: i18n.BACK_TO_RULES,
}}
- badgeOptions={{ text: i18n.EXPERIMENTAL }}
border
subtitle={subTitle}
subtitle2={[
@@ -262,34 +260,7 @@ const RuleDetailsComponent = memo(
>,
]
: []),
-
-
- {i18n.STATUS}
- {':'}
-
-
-
- {rule?.status ?? getEmptyTagValue()}
-
-
- {rule?.status_date && (
- <>
-
- <>{i18n.STATUS_AT}>
-
-
-
-
- >
- )}
- ,
+ ,
]}
title={title}
>
@@ -300,6 +271,7 @@ const RuleDetailsComponent = memo(
isDisabled={userHasNoPermissions}
enabled={rule?.enabled ?? false}
optionLabel={i18n.ACTIVATE_RULE}
+ onChange={handleOnChangeEnabledRule}
/>
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts
index 9976abc8412bf..46b6984ab323f 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts
@@ -13,7 +13,7 @@ export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.ruleDetails
export const BACK_TO_RULES = i18n.translate(
'xpack.siem.detectionEngine.ruleDetails.backToRulesDescription',
{
- defaultMessage: 'Back to rules',
+ defaultMessage: 'Back to signal detection rules',
}
);
@@ -35,24 +35,6 @@ export const UNKNOWN = i18n.translate('xpack.siem.detectionEngine.ruleDetails.un
defaultMessage: 'Unknown',
});
-export const STATUS = i18n.translate('xpack.siem.detectionEngine.ruleDetails.statusDescription', {
- defaultMessage: 'Status',
-});
-
-export const STATUS_AT = i18n.translate(
- 'xpack.siem.detectionEngine.ruleDetails.statusAtDescription',
- {
- defaultMessage: 'at',
- }
-);
-
-export const STATUS_DATE = i18n.translate(
- 'xpack.siem.detectionEngine.ruleDetails.statusDateDescription',
- {
- defaultMessage: 'Status date',
- }
-);
-
export const ERROR_CALLOUT_TITLE = i18n.translate(
'xpack.siem.detectionEngine.ruleDetails.errorCalloutTitle',
{
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx
index e583461f52439..9b7833afd7f4d 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx
@@ -17,11 +17,12 @@ import { FormattedMessage } from '@kbn/i18n/react';
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Redirect, useParams } from 'react-router-dom';
+import { useRule, usePersistRule } from '../../../../containers/detection_engine/rules';
import { HeaderPage } from '../../../../components/header_page';
import { WrapperPage } from '../../../../components/wrapper_page';
-import { SpyRoute } from '../../../../utils/route/spy_routes';
import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine';
-import { useRule, usePersistRule } from '../../../../containers/detection_engine/rules';
+import { displaySuccessToast, useStateToaster } from '../../../../components/toasters';
+import { SpyRoute } from '../../../../utils/route/spy_routes';
import { useUserInfo } from '../../components/user_info';
import { FormHook, FormData } from '../components/shared_imports';
import { StepPanel } from '../components/step_panel';
@@ -48,6 +49,7 @@ interface ScheduleStepRuleForm extends StepRuleForm {
}
export const EditRuleComponent = memo(() => {
+ const [, dispatchToaster] = useStateToaster();
const {
loading: initLoading,
isSignalIndexExists,
@@ -271,6 +273,7 @@ export const EditRuleComponent = memo(() => {
}, []);
if (isSaved || (rule != null && rule.immutable)) {
+ displaySuccessToast(i18n.SUCCESSFULLY_SAVED_RULE(rule?.name ?? ''), dispatchToaster);
return ;
}
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts
index b81ae58e565f0..f6e56dca19c21 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts
@@ -28,3 +28,9 @@ export const SORRY_ERRORS = i18n.translate(
export const BACK_TO = i18n.translate('xpack.siem.detectionEngine.editRule.backToDescription', {
defaultMessage: 'Back to',
});
+
+export const SUCCESSFULLY_SAVED_RULE = (ruleName: string) =>
+ i18n.translate('xpack.siem.detectionEngine.rules.update.successfullySavedRuleTitle', {
+ values: { ruleName },
+ defaultMessage: '{ruleName} was saved',
+ });
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx
index cc0882dd7e426..cfe6cb8da1cb0 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx
@@ -4,11 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { pick } from 'lodash/fp';
+import { get, pick } from 'lodash/fp';
import { useLocation } from 'react-router-dom';
import { esFilters } from '../../../../../../../../src/plugins/data/public';
import { Rule } from '../../../containers/detection_engine/rules';
+import { FormData, FormHook, FormSchema } from './components/shared_imports';
import { AboutStepRule, DefineStepRule, IMitreEnterpriseAttack, ScheduleStepRule } from './types';
interface GetStepsData {
@@ -67,3 +68,15 @@ export const getStepsData = ({
};
export const useQuery = () => new URLSearchParams(useLocation().search);
+
+export const setFieldValue = (
+ form: FormHook,
+ schema: FormSchema,
+ defaultValues: unknown
+) =>
+ Object.keys(schema).forEach(key => {
+ const val = get(key, defaultValues);
+ if (val != null) {
+ form.setFieldValue(key, val);
+ }
+ });
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts
index 83479b819f81e..e1257007d44a3 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts
@@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n';
export const BACK_TO_DETECTION_ENGINE = i18n.translate(
'xpack.siem.detectionEngine.rules.backOptionsHeader',
{
- defaultMessage: 'Back to detection engine',
+ defaultMessage: 'Back to detections',
}
);
@@ -18,7 +18,7 @@ export const IMPORT_RULE = i18n.translate('xpack.siem.detectionEngine.rules.impo
});
export const ADD_NEW_RULE = i18n.translate('xpack.siem.detectionEngine.rules.addNewRuleTitle', {
- defaultMessage: 'Add new rule',
+ defaultMessage: 'Create new rule',
});
export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.rules.pageTitle', {
@@ -32,7 +32,7 @@ export const REFRESH = i18n.translate('xpack.siem.detectionEngine.rules.allRules
export const BATCH_ACTIONS = i18n.translate(
'xpack.siem.detectionEngine.rules.allRules.batchActionsTitle',
{
- defaultMessage: 'Batch actions',
+ defaultMessage: 'Bulk actions',
}
);
@@ -243,7 +243,7 @@ export const COLUMN_TAGS = i18n.translate(
export const COLUMN_ACTIVATE = i18n.translate(
'xpack.siem.detectionEngine.rules.allRules.columns.activateTitle',
{
- defaultMessage: 'Activate',
+ defaultMessage: 'Activated',
}
);
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts
index d1935b4fd581d..ab785a8ad2c6d 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts
@@ -6,8 +6,8 @@
import { i18n } from '@kbn/i18n';
-export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.pageTitle', {
- defaultMessage: 'Detection engine',
+export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.detectionsPageTitle', {
+ defaultMessage: 'Detections',
});
export const LAST_SIGNAL = i18n.translate('xpack.siem.detectionEngine.lastSignalTitle', {
diff --git a/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx b/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx
index 220f8a958aa43..c0e959c5e97fa 100644
--- a/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx
@@ -36,12 +36,12 @@ export const navTabs: SiemNavTab = {
disabled: false,
urlKey: 'network',
},
- [SiemPageName.detectionEngine]: {
- id: SiemPageName.detectionEngine,
+ [SiemPageName.detections]: {
+ id: SiemPageName.detections,
name: i18n.DETECTION_ENGINE,
href: getDetectionEngineUrl(),
disabled: false,
- urlKey: 'detection-engine',
+ urlKey: 'detections',
},
[SiemPageName.timelines]: {
id: SiemPageName.timelines,
diff --git a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx
index a545be447796d..b5bfdbde306ca 100644
--- a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx
@@ -105,7 +105,7 @@ export const HomePage: React.FC = () => (
)}
/>
(
)}
diff --git a/x-pack/legacy/plugins/siem/public/pages/home/translations.ts b/x-pack/legacy/plugins/siem/public/pages/home/translations.ts
index b87ea1c17a117..80800a3bd4198 100644
--- a/x-pack/legacy/plugins/siem/public/pages/home/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/home/translations.ts
@@ -19,7 +19,7 @@ export const NETWORK = i18n.translate('xpack.siem.navigation.network', {
});
export const DETECTION_ENGINE = i18n.translate('xpack.siem.navigation.detectionEngine', {
- defaultMessage: 'Detection engine',
+ defaultMessage: 'Detections',
});
export const TIMELINES = i18n.translate('xpack.siem.navigation.timelines', {
diff --git a/x-pack/legacy/plugins/siem/public/pages/home/types.ts b/x-pack/legacy/plugins/siem/public/pages/home/types.ts
index 101c6a69b08d1..678de6dbcc128 100644
--- a/x-pack/legacy/plugins/siem/public/pages/home/types.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/home/types.ts
@@ -10,7 +10,7 @@ export enum SiemPageName {
overview = 'overview',
hosts = 'hosts',
network = 'network',
- detectionEngine = 'detection-engine',
+ detections = 'detections',
timelines = 'timelines',
}
@@ -18,7 +18,7 @@ export type SiemNavTabKey =
| SiemPageName.overview
| SiemPageName.hosts
| SiemPageName.network
- | SiemPageName.detectionEngine
+ | SiemPageName.detections
| SiemPageName.timelines;
export type SiemNavTab = Record;
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts
index 0c6ab1c82bcb8..a84fcb64d9ff7 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts
@@ -391,7 +391,7 @@ export const getMockPrivileges = () => ({
},
},
application: {},
- isAuthenticated: false,
+ is_authenticated: false,
});
export const getFindResultStatus = (): SavedObjectsFindResponse => ({
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts
index 240200af8b585..803d9d645aadb 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts
@@ -30,7 +30,7 @@ export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.Serve
const index = getIndex(request, server);
const permissions = await readPrivileges(callWithRequest, index);
return merge(permissions, {
- isAuthenticated: request?.auth?.isAuthenticated ?? false,
+ is_authenticated: request?.auth?.isAuthenticated ?? false,
});
} catch (err) {
return transformError(err);
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts
index 5ceecdb058e5f..3c9cad8dc4d4b 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts
@@ -36,8 +36,10 @@ export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerR
const actionsClient = isFunction(request.getActionsClient)
? request.getActionsClient()
: null;
-
- if (!alertsClient || !actionsClient) {
+ const savedObjectsClient = isFunction(request.getSavedObjectsClient)
+ ? request.getSavedObjectsClient()
+ : null;
+ if (!alertsClient || !actionsClient || !savedObjectsClient) {
return headers.response().code(404);
}
@@ -59,7 +61,13 @@ export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerR
}
}
await installPrepackagedRules(alertsClient, actionsClient, rulesToInstall, spaceIndex);
- await updatePrepackagedRules(alertsClient, actionsClient, rulesToUpdate, spaceIndex);
+ await updatePrepackagedRules(
+ alertsClient,
+ actionsClient,
+ savedObjectsClient,
+ rulesToUpdate,
+ spaceIndex
+ );
return {
rules_installed: rulesToInstall.length,
rules_updated: rulesToUpdate.length,
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts
index e56c440f5a415..545c2e488b1c8 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts
@@ -13,10 +13,16 @@ import { findRulesStatusesSchema } from '../schemas/find_rules_statuses_schema';
import {
FindRulesStatusesRequest,
IRuleSavedAttributesSavedObjectAttributes,
+ RuleStatusResponse,
+ IRuleStatusAttributes,
} from '../../rules/types';
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
-const convertToSnakeCase = (obj: IRuleSavedAttributesSavedObjectAttributes) => {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const convertToSnakeCase = >(obj: T): Partial | null => {
+ if (!obj) {
+ return null;
+ }
return Object.keys(obj).reduce((acc, item) => {
const newKey = snakeCase(item);
return { ...acc, [newKey]: obj[item] };
@@ -53,7 +59,7 @@ export const createFindRulesStatusRoute: Hapi.ServerRoute = {
"anotherAlertId": ...
}
*/
- const statuses = await query.ids.reduce(async (acc, id) => {
+ const statuses = await query.ids.reduce>(async (acc, id) => {
const lastFiveErrorsForId = await savedObjectsClient.find<
IRuleSavedAttributesSavedObjectAttributes
>({
@@ -64,15 +70,21 @@ export const createFindRulesStatusRoute: Hapi.ServerRoute = {
search: id,
searchFields: ['alertId'],
});
- const toDisplay =
- lastFiveErrorsForId.saved_objects.length <= 5
- ? lastFiveErrorsForId.saved_objects
- : lastFiveErrorsForId.saved_objects.slice(1);
+ const accumulated = await acc;
+ const currentStatus = convertToSnakeCase(
+ lastFiveErrorsForId.saved_objects[0]?.attributes
+ );
+ const failures = lastFiveErrorsForId.saved_objects
+ .slice(1)
+ .map(errorItem => convertToSnakeCase(errorItem.attributes));
return {
- ...(await acc),
- [id]: toDisplay.map(errorItem => convertToSnakeCase(errorItem.attributes)),
+ ...accumulated,
+ [id]: {
+ current_status: currentStatus,
+ failures,
+ },
};
- }, {});
+ }, Promise.resolve({}));
return statuses;
},
};
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts
index e312b5fc6bb10..6efaa1fea60d0 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts
@@ -52,8 +52,10 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
const actionsClient = isFunction(request.getActionsClient)
? request.getActionsClient()
: null;
-
- if (!alertsClient || !actionsClient) {
+ const savedObjectsClient = isFunction(request.getSavedObjectsClient)
+ ? request.getSavedObjectsClient()
+ : null;
+ if (!alertsClient || !actionsClient || !savedObjectsClient) {
return headers.response().code(404);
}
const { filename } = request.payload.file.hapi;
@@ -161,6 +163,7 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
const updatedRule = await updateRules({
alertsClient,
actionsClient,
+ savedObjectsClient,
description,
enabled,
falsePositives,
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts
index b01108f0de21f..e0d2672cf356a 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts
@@ -7,12 +7,16 @@
import Hapi from 'hapi';
import { isFunction } from 'lodash/fp';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
-import { BulkUpdateRulesRequest } from '../../rules/types';
+import {
+ BulkUpdateRulesRequest,
+ IRuleSavedAttributesSavedObjectAttributes,
+} from '../../rules/types';
import { ServerFacade } from '../../../../types';
import { transformOrBulkError, getIdBulkError } from './utils';
import { transformBulkError } from '../utils';
import { updateRulesBulkSchema } from '../schemas/update_rules_bulk_schema';
import { updateRules } from '../../rules/update_rules';
+import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => {
return {
@@ -32,8 +36,10 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou
const actionsClient = isFunction(request.getActionsClient)
? request.getActionsClient()
: null;
-
- if (!alertsClient || !actionsClient) {
+ const savedObjectsClient = isFunction(request.getSavedObjectsClient)
+ ? request.getSavedObjectsClient()
+ : null;
+ if (!alertsClient || !actionsClient || !savedObjectsClient) {
return headers.response().code(404);
}
@@ -80,6 +86,7 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou
language,
outputIndex,
savedId,
+ savedObjectsClient,
timelineId,
timelineTitle,
meta,
@@ -100,7 +107,17 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou
version,
});
if (rule != null) {
- return transformOrBulkError(rule.id, rule);
+ const ruleStatuses = await savedObjectsClient.find<
+ IRuleSavedAttributesSavedObjectAttributes
+ >({
+ type: ruleStatusSavedObjectType,
+ perPage: 1,
+ sortField: 'statusDate',
+ sortOrder: 'desc',
+ search: rule.id,
+ searchFields: ['alertId'],
+ });
+ return transformOrBulkError(rule.id, rule, ruleStatuses.saved_objects[0]);
} else {
return getIdBulkError({ id, ruleId });
}
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts
index 533fe9b724943..49c9304ae2d25 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts
@@ -78,6 +78,7 @@ export const createUpdateRulesRoute: Hapi.ServerRoute = {
language,
outputIndex,
savedId,
+ savedObjectsClient,
timelineId,
timelineTitle,
meta,
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts
index 5a3f19c0bf0ef..e238e6398845c 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts
@@ -7,7 +7,12 @@
import { get } from 'lodash/fp';
import { Readable } from 'stream';
-import { SavedObject, SavedObjectAttributes, SavedObjectsFindResponse } from 'kibana/server';
+import {
+ SavedObject,
+ SavedObjectAttributes,
+ SavedObjectsFindResponse,
+ SavedObjectsClientContract,
+} from 'kibana/server';
import { SIGNALS_ID } from '../../../../common/constants';
import { AlertsClient } from '../../../../../alerting/server/alerts_client';
import { ActionsClient } from '../../../../../actions/server/actions_client';
@@ -41,14 +46,22 @@ export interface RuleAlertType extends Alert {
params: RuleTypeParams;
}
-export interface IRuleStatusAttributes {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export interface IRuleStatusAttributes extends Record {
alertId: string; // created alert id.
statusDate: string;
lastFailureAt: string | null | undefined;
lastFailureMessage: string | null | undefined;
lastSuccessAt: string | null | undefined;
lastSuccessMessage: string | null | undefined;
- status: RuleStatusString;
+ status: RuleStatusString | null | undefined;
+}
+
+export interface RuleStatusResponse {
+ [key: string]: {
+ current_status: IRuleStatusAttributes | null | undefined;
+ failures: IRuleStatusAttributes[] | null | undefined;
+ };
}
export interface IRuleSavedAttributesSavedObjectAttributes
@@ -142,6 +155,7 @@ export interface Clients {
export type UpdateRuleParams = Partial & {
id: string | undefined | null;
+ savedObjectsClient: SavedObjectsClientContract;
} & Clients;
export type DeleteRuleParams = Clients & {
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts
index 756634c8fa042..0d7fb7918b67e 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { SavedObjectsClientContract } from 'kibana/server';
import { ActionsClient } from '../../../../../actions';
import { AlertsClient } from '../../../../../alerting';
import { updateRules } from './update_rules';
@@ -12,6 +13,7 @@ import { PrepackagedRules } from '../types';
export const updatePrepackagedRules = async (
alertsClient: AlertsClient,
actionsClient: ActionsClient,
+ savedObjectsClient: SavedObjectsClientContract,
rules: PrepackagedRules[],
outputIndex: string
): Promise => {
@@ -55,6 +57,7 @@ export const updatePrepackagedRules = async (
outputIndex,
id: undefined, // We never have an id when updating from pre-packaged rules
savedId,
+ savedObjectsClient,
meta,
filters,
ruleId,
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts
index 0fe4b15437af8..e2632791f859e 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts
@@ -7,8 +7,9 @@
import { defaults } from 'lodash/fp';
import { AlertAction, IntervalSchedule } from '../../../../../alerting/server/types';
import { readRules } from './read_rules';
-import { UpdateRuleParams } from './types';
+import { UpdateRuleParams, IRuleSavedAttributesSavedObjectAttributes } from './types';
import { addTags } from './add_tags';
+import { ruleStatusSavedObjectType } from './saved_object_mappings';
export const calculateInterval = (
interval: string | undefined,
@@ -66,6 +67,7 @@ export const calculateName = ({
export const updateRules = async ({
alertsClient,
actionsClient, // TODO: Use this whenever we add feature support for different action types
+ savedObjectsClient,
description,
falsePositives,
enabled,
@@ -135,10 +137,39 @@ export const updateRules = async ({
}
);
+ const ruleCurrentStatus = savedObjectsClient
+ ? await savedObjectsClient.find({
+ type: ruleStatusSavedObjectType,
+ perPage: 1,
+ sortField: 'statusDate',
+ sortOrder: 'desc',
+ search: rule.id,
+ searchFields: ['alertId'],
+ })
+ : null;
+
if (rule.enabled && enabled === false) {
await alertsClient.disable({ id: rule.id });
+ // set current status for this rule to null to represent disabled,
+ // but keep last_success_at / last_failure_at properties intact for
+ // use on frontend while rule is disabled.
+ if (ruleCurrentStatus && ruleCurrentStatus.saved_objects.length > 0) {
+ const currentStatusToDisable = ruleCurrentStatus.saved_objects[0];
+ currentStatusToDisable.attributes.status = null;
+ await savedObjectsClient?.update(ruleStatusSavedObjectType, currentStatusToDisable.id, {
+ ...currentStatusToDisable.attributes,
+ });
+ }
} else if (!rule.enabled && enabled === true) {
await alertsClient.enable({ id: rule.id });
+ // set current status for this rule to be 'going to run'
+ if (ruleCurrentStatus && ruleCurrentStatus.saved_objects.length > 0) {
+ const currentStatusToDisable = ruleCurrentStatus.saved_objects[0];
+ currentStatusToDisable.attributes.status = 'going to run';
+ await savedObjectsClient?.update(ruleStatusSavedObjectType, currentStatusToDisable.id, {
+ ...currentStatusToDisable.attributes,
+ });
+ }
} else {
// enabled is null or undefined and we do not touch the rule
}
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts
index d80eadd2c088b..32f2c86914770 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts
@@ -96,7 +96,7 @@ export const signalRulesAlertType = ({
>(ruleStatusSavedObjectType, {
alertId, // do a search for this id.
statusDate: date,
- status: 'executing',
+ status: 'going to run',
lastFailureAt: null,
lastSuccessAt: null,
lastFailureMessage: null,
@@ -106,7 +106,7 @@ export const signalRulesAlertType = ({
// update 0th to executing.
currentStatusSavedObject = ruleStatusSavedObjects.saved_objects[0];
const sDate = new Date().toISOString();
- currentStatusSavedObject.attributes.status = 'executing';
+ currentStatusSavedObject.attributes.status = 'going to run';
currentStatusSavedObject.attributes.statusDate = sDate;
await services.savedObjectsClient.update(
ruleStatusSavedObjectType,
diff --git a/x-pack/legacy/plugins/uptime/public/contexts/uptime_settings_context.tsx b/x-pack/legacy/plugins/uptime/public/contexts/uptime_settings_context.tsx
index b529d5346e88e..4b3abb46ac1e4 100644
--- a/x-pack/legacy/plugins/uptime/public/contexts/uptime_settings_context.tsx
+++ b/x-pack/legacy/plugins/uptime/public/contexts/uptime_settings_context.tsx
@@ -37,7 +37,7 @@ const defaultContext: UptimeSettingsContextValues = {
export const UptimeSettingsContext = createContext(defaultContext);
export const UptimeSettingsContextProvider: React.FC = ({ children, ...props }) => {
- const { basePath, isApmAvailable, isInfraAvailable, isLogsAvailable } = props;
+ const { basePath, isApmAvailable, isInfraAvailable, isLogsAvailable, commonlyUsedRanges } = props;
const { dateRangeStart, dateRangeEnd } = useParams();
@@ -47,10 +47,19 @@ export const UptimeSettingsContextProvider: React.FC = ({ childr
isApmAvailable,
isInfraAvailable,
isLogsAvailable,
+ commonlyUsedRanges,
dateRangeStart: dateRangeStart ?? DATE_RANGE_START,
dateRangeEnd: dateRangeEnd ?? DATE_RANGE_END,
};
- }, [basePath, isApmAvailable, isInfraAvailable, isLogsAvailable, dateRangeStart, dateRangeEnd]);
+ }, [
+ basePath,
+ isApmAvailable,
+ isInfraAvailable,
+ isLogsAvailable,
+ dateRangeStart,
+ dateRangeEnd,
+ commonlyUsedRanges,
+ ]);
return ;
};
diff --git a/x-pack/plugins/endpoint/server/plugin.test.ts b/x-pack/plugins/endpoint/server/plugin.test.ts
index 87d373d3a4f34..7dd878d579043 100644
--- a/x-pack/plugins/endpoint/server/plugin.test.ts
+++ b/x-pack/plugins/endpoint/server/plugin.test.ts
@@ -3,14 +3,14 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { CoreSetup } from 'kibana/server';
+
import { EndpointPlugin, EndpointPluginSetupDependencies } from './plugin';
import { coreMock } from '../../../../src/core/server/mocks';
import { PluginSetupContract } from '../../features/server';
describe('test endpoint plugin', () => {
let plugin: EndpointPlugin;
- let mockCoreSetup: MockedKeys;
+ let mockCoreSetup: ReturnType;
let mockedEndpointPluginSetupDependencies: jest.Mocked;
let mockedPluginSetupContract: jest.Mocked;
beforeEach(() => {
diff --git a/x-pack/plugins/licensing/server/plugin.test.ts b/x-pack/plugins/licensing/server/plugin.test.ts
index 9547a2dc52966..c9fbb61e6cc19 100644
--- a/x-pack/plugins/licensing/server/plugin.test.ts
+++ b/x-pack/plugins/licensing/server/plugin.test.ts
@@ -261,7 +261,6 @@ describe('licensing plugin', () => {
expect(customClient.callAsInternalUser).toHaveBeenCalledTimes(1);
await flushPromises(customPollingFrequency * 1.5);
- expect(customClient.callAsInternalUser).toHaveBeenCalledTimes(2);
expect(customLicense.isAvailable).toBe(true);
expect(customLicense.type).toBe('gold');
diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts
index 5e32a0e90198a..56aad4ece3e95 100644
--- a/x-pack/plugins/security/server/plugin.test.ts
+++ b/x-pack/plugins/security/server/plugin.test.ts
@@ -6,7 +6,7 @@
import { of } from 'rxjs';
import { ByteSizeValue } from '@kbn/config-schema';
-import { ICustomClusterClient, CoreSetup } from '../../../../src/core/server';
+import { ICustomClusterClient } from '../../../../src/core/server';
import { elasticsearchClientPlugin } from './elasticsearch_client_plugin';
import { Plugin, PluginSetupDependencies } from './plugin';
@@ -14,7 +14,7 @@ import { coreMock, elasticsearchServiceMock } from '../../../../src/core/server/
describe('Security Plugin', () => {
let plugin: Plugin;
- let mockCoreSetup: MockedKeys;
+ let mockCoreSetup: ReturnType;
let mockClusterClient: jest.Mocked;
let mockDependencies: PluginSetupDependencies;
beforeEach(() => {
diff --git a/x-pack/plugins/task_manager/server/task_manager.ts b/x-pack/plugins/task_manager/server/task_manager.ts
index 93e98f33a30b0..da9640fa3e071 100644
--- a/x-pack/plugins/task_manager/server/task_manager.ts
+++ b/x-pack/plugins/task_manager/server/task_manager.ts
@@ -401,7 +401,7 @@ export async function claimAvailableTasks(
} else {
performance.mark('claimAvailableTasks.noAvailableWorkers');
logger.info(
- `[Task Ownership]: Task Manager has skipped Claiming Ownership of available tasks at it has ran out Available Workers. If this happens often, consider adjusting the "xpack.task_manager.max_workers" configuration.`
+ `[Task Ownership]: Task Manager has skipped Claiming Ownership of available tasks at it has ran out Available Workers.`
);
}
return [];