diff --git a/docs/maps/geojson-upload.asciidoc b/docs/maps/geojson-upload.asciidoc
index d9b21f591f625..8c3cb371b6add 100644
--- a/docs/maps/geojson-upload.asciidoc
+++ b/docs/maps/geojson-upload.asciidoc
@@ -9,7 +9,7 @@ for example, in visualizations and Canvas workpads.
[float]
=== Why GeoJSON?
GeoJSON is an open-standard file format for storing geospatial vector data.
-Although many vector data formats are available in the GIS community,
+Although many vector data formats are available in the GIS community,
GeoJSON is the most commonly used and flexible option.
[float]
@@ -18,14 +18,14 @@ Follow the instructions below to upload a GeoJSON data file, or try the
<>.
. Open *Elastic Maps*, and then click *Add layer*.
-. Click *Upload GeoJSON vector file*.
+. Click *Uploaded GeoJSON*.
+
[role="screenshot"]
image::maps/images/fu_gs_select_source_file_upload.png[]
. Use the file chooser to select a valid GeoJSON file. The file will load
a preview of the data on the map.
-. Use the default *Index type* of {ref}/geo-point.html[geo_point] for point data,
+. Use the default *Index type* of {ref}/geo-point.html[geo_point] for point data,
or override it and select {ref}/geo-shape.html[geo_shape].
All other shapes will default to a type of `geo_shape`.
. Leave the default *Index name* and *Index pattern* names (the name of the uploaded
diff --git a/docs/maps/indexing-geojson-data-tutorial.asciidoc b/docs/maps/indexing-geojson-data-tutorial.asciidoc
index 5645567ec7e79..5284bd9ac2ac5 100644
--- a/docs/maps/indexing-geojson-data-tutorial.asciidoc
+++ b/docs/maps/indexing-geojson-data-tutorial.asciidoc
@@ -2,7 +2,7 @@
[[indexing-geojson-data-tutorial]]
== Indexing GeoJSON data tutorial
-In this tutorial, you'll build a customized map that shows the flight path between
+In this tutorial, you'll build a customized map that shows the flight path between
two airports, and the lightning hot spots on that route. You'll learn to:
* Import GeoJSON files into Kibana
@@ -15,7 +15,7 @@ two airports, and the lightning hot spots on that route. You'll learn to:
This tutorial requires you to download the following GeoJSON sample data files.
These files are good examples of the types
of vector data that you can upload to Kibana and index in
-Elasticsearch for display in *Elastic Maps*.
+Elasticsearch for display in *Elastic Maps*.
* https://raw.githubusercontent.com/elastic/examples/master/Maps/Getting%20Started%20Examples/geojson_upload_and_styling/logan_international_airport.geojson[Logan International Airport]
* https://raw.githubusercontent.com/elastic/examples/master/Maps/Getting%20Started%20Examples/geojson_upload_and_styling/bangor_international_airport.geojson[Bangor International Airport]
@@ -23,7 +23,7 @@ Elasticsearch for display in *Elastic Maps*.
* https://raw.githubusercontent.com/elastic/examples/master/Maps/Getting%20Started%20Examples/geojson_upload_and_styling/original_flight_path.geojson[Original flight path]
* https://raw.githubusercontent.com/elastic/examples/master/Maps/Getting%20Started%20Examples/geojson_upload_and_styling/modified_flight_path.geojson[Modified flight path]
-The data represents two real airports, two fictitious flight routes, and
+The data represents two real airports, two fictitious flight routes, and
fictitious lightning reports. You don't need to use all of
these files. Feel free to work with as many files as you'd like, or use valid GeoJSON
files of your own.
@@ -47,12 +47,12 @@ image::maps/images/fu_gs_new_england_map.png[]
For each GeoJSON file you downloaded, complete the following steps:
. Below the map legend, click *Add layer*.
-. From the list of layer types, click *Upload GeoJSON vector file*.
+. From the list of layer types, click *Uploaded GeoJSON*.
. Using the File Picker, upload the GeoJSON file.
+
-Depending on the geometry type of your features, this will
+Depending on the geometry type of your features, this will
auto-populate *Index type* with either {ref}/geo-point.html[geo_point] or
- {ref}/geo-shape.html[geo_shape] and *Index name* with
+ {ref}/geo-shape.html[geo_shape] and *Index name* with
``.
. Click *Import file* in the lower right.
@@ -60,7 +60,7 @@ auto-populate *Index type* with either {ref}/geo-point.html[geo_point] or
You'll see activity as the GeoJSON Upload utility creates a new index
and index pattern for the data set. When the process is complete, you should
receive messages that the creation of the new index and index pattern
-were successful.
+were successful.
. Click *Add layer* in the bottom right.
@@ -69,7 +69,7 @@ were successful.
. Once you've added all of the sample files,
<>.
+
-At this point, you could consider the map complete,
+At this point, you could consider the map complete,
but there are a few additions and tweaks that you can make to tell a
better story with your data.
+
@@ -80,18 +80,18 @@ image::maps/images/fu_gs_flight_paths.png[]
=== Add a heatmap aggregation layer
Looking at the `Lightning detected` layer, it's clear where lightning has
-struck. What's less clear, is if there have been more lightning
-strikes in some areas than others, in other words, where the lightning
+struck. What's less clear, is if there have been more lightning
+strikes in some areas than others, in other words, where the lightning
hot spots are. An advantage of having indexed
-{ref}/geo-point.html[geo_point] data for the
-lightning strikes is that you can perform aggregations on the data.
+{ref}/geo-point.html[geo_point] data for the
+lightning strikes is that you can perform aggregations on the data.
. Below the map legend, click *Add layer*.
. From the list of layer types, click *Grid aggregation*.
+
-Because you indexed `lightning_detected.geojson` using the index name and
+Because you indexed `lightning_detected.geojson` using the index name and
pattern `lightning_detected`, that data is available as a {ref}/geo-point.html[geo_point]
-aggregation.
+aggregation.
. Select `lightning_detected`.
. Click *Show as* and select `heat map`.
@@ -99,15 +99,15 @@ aggregation.
"Lightning intensity".
+
The remaining default settings are good, but there are a couple of
-settings that you might want to change.
+settings that you might want to change.
-. Under *Source settings* > *Grid resolution*, select from the different heat map resolutions.
+. Under *Source settings* > *Grid resolution*, select from the different heat map resolutions.
+
The default "Coarse" looks
good, but feel free to select a different resolution.
. Play around with the *Layer Style* >
-*Color range* setting.
+*Color range* setting.
+
Again the default looks good, but feel free to choose a
different color range.
@@ -125,14 +125,14 @@ image::maps/images/fu_gs_lightning_intensity.png[]
=== Organize the layers
Consider ways you might improve the appearance of the final map.
-Small changes in how and when layers are shown can help tell a
+Small changes in how and when layers are shown can help tell a
better story with your data. Here are a few final tweaks
you might make:
* Update layer names
* Adjust styles for each layer
* Adjust the layer order
-* Decide which layers to show at different zoom levels
+* Decide which layers to show at different zoom levels
When you've finished, again be sure to <>.
diff --git a/docs/maps/maps-getting-started.asciidoc b/docs/maps/maps-getting-started.asciidoc
index f6db2f0fff219..88ad6a26d3697 100644
--- a/docs/maps/maps-getting-started.asciidoc
+++ b/docs/maps/maps-getting-started.asciidoc
@@ -65,7 +65,7 @@ and lighter shades symbolize countries with less traffic.
==== Add a vector layer from the Elastic Maps Service source
. In the map legend, click *Add layer*.
-. Click the *Vector shapes* data source.
+. Click the *EMS Boundaries* data source.
. From the *Layer* dropdown menu, select *World Countries*.
. Click the *Add layer* button.
. Set *Layer name* to `Total Requests by Country`.
diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts
index 58fe2b52727ef..6c4a1f263bc71 100644
--- a/src/core/server/elasticsearch/elasticsearch_service.test.ts
+++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts
@@ -174,11 +174,55 @@ Object {
undefined,
],
"ssl": Object {
+ "certificateAuthorities": undefined,
"verificationMode": "none",
},
}
`);
});
+
+ it('does not merge elasticsearch hosts if custom config overrides', async () => {
+ configService.atPath.mockReturnValueOnce(
+ new BehaviorSubject({
+ hosts: ['http://1.2.3.4', 'http://9.8.7.6'],
+ healthCheck: {
+ delay: 2000,
+ },
+ ssl: {
+ verificationMode: 'none',
+ },
+ } as any)
+ );
+ elasticsearchService = new ElasticsearchService(coreContext);
+ const setupContract = await elasticsearchService.setup(deps);
+ // reset all mocks called during setup phase
+ MockClusterClient.mockClear();
+
+ const customConfig = {
+ hosts: ['http://8.8.8.8'],
+ logQueries: true,
+ ssl: { certificate: 'certificate-value' },
+ };
+ setupContract.createClient('some-custom-type', customConfig);
+
+ const config = MockClusterClient.mock.calls[0][0];
+ expect(config).toMatchInlineSnapshot(`
+ Object {
+ "healthCheckDelay": 2000,
+ "hosts": Array [
+ "http://8.8.8.8",
+ ],
+ "logQueries": true,
+ "requestHeadersWhitelist": Array [
+ undefined,
+ ],
+ "ssl": Object {
+ "certificate": "certificate-value",
+ "verificationMode": "none",
+ },
+ }
+ `);
+ });
});
});
diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts
index ed1f2a276ebc8..1f062412edaf2 100644
--- a/src/core/server/elasticsearch/elasticsearch_service.ts
+++ b/src/core/server/elasticsearch/elasticsearch_service.ts
@@ -19,8 +19,9 @@
import { ConnectableObservable, Observable, Subscription } from 'rxjs';
import { filter, first, map, publishReplay, switchMap } from 'rxjs/operators';
-import { merge } from 'lodash';
+
import { CoreService } from '../../types';
+import { merge } from '../../utils';
import { CoreContext } from '../core_context';
import { Logger } from '../logging';
import { ClusterClient } from './cluster_client';
diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts
index b6ffb57db975c..98f0800feae79 100644
--- a/src/core/utils/index.ts
+++ b/src/core/utils/index.ts
@@ -22,5 +22,6 @@ export * from './context';
export * from './deep_freeze';
export * from './get';
export * from './map_to_object';
+export * from './merge';
export * from './pick';
export * from './url';
diff --git a/src/core/utils/merge.test.ts b/src/core/utils/merge.test.ts
new file mode 100644
index 0000000000000..aa98f51067411
--- /dev/null
+++ b/src/core/utils/merge.test.ts
@@ -0,0 +1,64 @@
+/*
+ * 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 { merge } from './merge';
+
+describe('merge', () => {
+ test('empty objects', () => expect(merge({}, {})).toEqual({}));
+
+ test('basic', () => {
+ expect(merge({}, { a: 1 })).toEqual({ a: 1 });
+ expect(merge({ a: 0 }, {})).toEqual({ a: 0 });
+ expect(merge({ a: 0 }, { a: 1 })).toEqual({ a: 1 });
+ });
+
+ test('undefined', () => {
+ expect(merge({ a: undefined }, { a: 1 })).toEqual({ a: 1 });
+ expect(merge({ a: 0 }, { a: undefined })).toEqual({ a: 0 });
+ expect(merge({ a: undefined }, { a: undefined })).toEqual({});
+ expect(merge({ a: void 0 }, { a: void 0 })).toEqual({});
+ });
+
+ test('null', () => {
+ expect(merge({ a: null }, { a: 1 })).toEqual({ a: 1 });
+ expect(merge({ a: 0 }, { a: null })).toEqual({ a: null });
+ expect(merge({ a: null }, { a: null })).toEqual({ a: null });
+ });
+
+ test('arrays', () => {
+ expect(merge({ b: [0] }, { b: [2] })).toEqual({ b: [2] });
+ expect(merge({ b: [0, 1] }, { b: [2] })).toEqual({ b: [2] });
+ expect(merge({ b: [0] }, { b: [2, 3] })).toEqual({ b: [2, 3] });
+ expect(merge({ b: [] }, { b: [2] })).toEqual({ b: [2] });
+ expect(merge({ b: [0] }, { b: [] })).toEqual({ b: [] });
+ });
+
+ test('nested objects', () => {
+ expect(merge({ top: { a: 0, b: 0 } }, { top: { a: 1, c: 1 } })).toEqual({
+ top: { a: 1, b: 0, c: 1 },
+ });
+ expect(merge({ top: { a: 0, b: 0 } }, { top: [0, 1] })).toEqual({ top: [0, 1] });
+ });
+
+ test('multiple objects', () => {
+ expect(merge({}, { a: 1 }, { a: 2 })).toEqual({ a: 2 });
+ expect(merge({ a: 0 }, {}, {})).toEqual({ a: 0 });
+ expect(merge({ a: 0 }, { a: 1 }, {})).toEqual({ a: 1 });
+ });
+});
diff --git a/src/core/utils/merge.ts b/src/core/utils/merge.ts
new file mode 100644
index 0000000000000..aead3f35ba841
--- /dev/null
+++ b/src/core/utils/merge.ts
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+
+/**
+ * Deeply merges two objects, omitting undefined values, and not deeply merging Arrays.
+ *
+ * @remarks
+ * Should behave identically to lodash.merge, however it will not merge Array values like lodash does.
+ * Any properties with `undefined` values on both objects will be ommitted from the returned object.
+ */
+export function merge, TSource1 extends Record>(
+ baseObj: TBase,
+ source1: TSource1
+): TBase & TSource1;
+export function merge<
+ TBase extends Record,
+ TSource1 extends Record,
+ TSource2 extends Record
+>(baseObj: TBase, overrideObj: TSource1, overrideObj2: TSource2): TBase & TSource1 & TSource2;
+export function merge<
+ TBase extends Record,
+ TSource1 extends Record,
+ TSource2 extends Record,
+ TSource3 extends Record
+>(
+ baseObj: TBase,
+ overrideObj: TSource1,
+ overrideObj2: TSource2
+): TBase & TSource1 & TSource2 & TSource3;
+export function merge>(
+ baseObj: Record,
+ ...sources: Array>
+): TReturn {
+ const firstSource = sources[0];
+ if (firstSource === undefined) {
+ return baseObj as TReturn;
+ }
+
+ return sources
+ .slice(1)
+ .reduce(
+ (merged, nextSource) => mergeObjects(merged, nextSource),
+ mergeObjects(baseObj, firstSource)
+ ) as TReturn;
+}
+
+const isMergable = (obj: any) => typeof obj === 'object' && obj !== null && !Array.isArray(obj);
+
+const mergeObjects = , U extends Record>(
+ baseObj: T,
+ overrideObj: U
+): T & U =>
+ [...new Set([...Object.keys(baseObj), ...Object.keys(overrideObj)])].reduce(
+ (merged, key) => {
+ const baseVal = baseObj[key];
+ const overrideVal = overrideObj[key];
+
+ if (isMergable(baseVal) && isMergable(overrideVal)) {
+ merged[key] = mergeObjects(baseVal, overrideVal);
+ } else if (overrideVal !== undefined) {
+ merged[key] = overrideVal;
+ } else if (baseVal !== undefined) {
+ merged[key] = baseVal;
+ }
+
+ return merged;
+ },
+ {} as any
+ );
diff --git a/src/legacy/core_plugins/interpreter/public/renderers/visualization.ts b/src/legacy/core_plugins/interpreter/public/renderers/visualization.ts
index 90020f819dbe2..bedba6bfacede 100644
--- a/src/legacy/core_plugins/interpreter/public/renderers/visualization.ts
+++ b/src/legacy/core_plugins/interpreter/public/renderers/visualization.ts
@@ -38,7 +38,11 @@ export const visualization = () => ({
// special case in visualize, we need to render first (without executing the expression), for maps to work
if (visConfig) {
$rootScope.$apply(() => {
- handlers.vis.setCurrentState({ type: visType, params: visConfig });
+ handlers.vis.setCurrentState({
+ type: visType,
+ params: visConfig,
+ title: handlers.vis.title,
+ });
});
}
} else {
diff --git a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js
index dac0880e6fec4..b24cf447d21d6 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js
+++ b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js
@@ -600,6 +600,10 @@ function VisEditor(
} else if (savedVis.id === $route.current.params.id) {
docTitle.change(savedVis.lastSavedTitle);
chrome.breadcrumbs.set($injector.invoke(getEditBreadcrumbs));
+ savedVis.vis.title = savedVis.title;
+ savedVis.vis.description = savedVis.description;
+ // it's needed to save the state to update url string
+ $state.save();
} else {
kbnUrl.change(`${VisualizeConstants.EDIT_PATH}/{{id}}`, { id: savedVis.id });
}
diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js
index 9f37feef781ca..6c7170d6b2168 100644
--- a/test/functional/apps/discover/_discover.js
+++ b/test/functional/apps/discover/_discover.js
@@ -106,9 +106,9 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.discover.brushHistogram();
const newDurationHours = await PageObjects.timePicker.getTimeDurationInHours();
- expect(Math.round(newDurationHours)).to.be(108);
+ expect(Math.round(newDurationHours)).to.be(25);
const rowData = await PageObjects.discover.getDocTableField(1);
- expect(rowData).to.have.string('Sep 22, 2015 @ 23:50:13.253');
+ expect(Date.parse(rowData)).to.be.within(Date.parse('Sep 20, 2015 @ 22:00:00.000'), Date.parse('Sep 20, 2015 @ 23:30:00.000'));
});
it('should show correct initial chart interval of Auto', async function () {
diff --git a/test/functional/apps/visualize/_shared_item.js b/test/functional/apps/visualize/_shared_item.js
index efd534f035093..9fe38be15a741 100644
--- a/test/functional/apps/visualize/_shared_item.js
+++ b/test/functional/apps/visualize/_shared_item.js
@@ -24,8 +24,7 @@ export default function ({ getService, getPageObjects }) {
const retry = getService('retry');
const PageObjects = getPageObjects(['common', 'visualize']);
- // https://github.com/elastic/kibana/issues/37130
- describe.skip('data-shared-item', function indexPatternCreation() {
+ describe('data-shared-item', function indexPatternCreation() {
before(async function () {
log.debug('navigateToApp visualize');
await PageObjects.common.navigateToApp('visualize');
diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/chart.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/chart.tsx
index 73adcd13e2441..e4100c6d774b1 100644
--- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/chart.tsx
+++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/chart.tsx
@@ -94,6 +94,7 @@ export const AnomaliesChart: React.FunctionComponent<{
onBrushEnd={handleBrushEnd}
tooltip={tooltipProps}
baseTheme={isDarkMode ? DARK_THEME : LIGHT_THEME}
+ xDomain={{ min: timeRange.startTime, max: timeRange.endTime }}
/>
diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/bar_chart.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/bar_chart.tsx
index de856bee90513..5055e3fc08239 100644
--- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/bar_chart.tsx
+++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/bar_chart.tsx
@@ -90,6 +90,7 @@ export const LogEntryRateBarChart: React.FunctionComponent<{
theme={isDarkMode ? DARK_THEME : LIGHT_THEME}
showLegend
legendPosition="right"
+ xDomain={{ min: timeRange.startTime, max: timeRange.endTime }}
/>
diff --git a/x-pack/legacy/plugins/ml/common/types/fields.ts b/x-pack/legacy/plugins/ml/common/types/fields.ts
index ac9e8702e0945..9e1b992eec907 100644
--- a/x-pack/legacy/plugins/ml/common/types/fields.ts
+++ b/x-pack/legacy/plugins/ml/common/types/fields.ts
@@ -10,6 +10,7 @@ import {
KIBANA_AGGREGATION,
ES_AGGREGATION,
} from '../../common/constants/aggregation_types';
+import { MLCATEGORY } from '../../common/constants/field_types';
export const EVENT_RATE_FIELD_ID = '__ml_event_rate_count__';
export const METRIC_AGG_TYPE = 'metrics';
@@ -81,3 +82,10 @@ export interface AggFieldNamePair {
};
excludeFrequent?: string;
}
+
+export const mlCategory: Field = {
+ id: MLCATEGORY,
+ name: MLCATEGORY,
+ type: ES_FIELD_TYPES.KEYWORD,
+ aggregatable: false,
+};
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/advanced_job_creator.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/advanced_job_creator.ts
index 39f222e96abc0..54e704767b992 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/advanced_job_creator.ts
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/advanced_job_creator.ts
@@ -179,7 +179,7 @@ export class AdvancedJobCreator extends JobCreator {
public cloneFromExistingJob(job: Job, datafeed: Datafeed) {
this._overrideConfigs(job, datafeed);
- const detectors = getRichDetectors(job, datafeed, true);
+ const detectors = getRichDetectors(job, datafeed, this.scriptFields, 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/jobs/new_job_new/common/job_creator/job_creator.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/job_creator.ts
index ee6f30ca4f0d3..aa16da08e3a3a 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/job_creator.ts
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/job_creator.ts
@@ -8,6 +8,7 @@ import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/type
import { IndexPattern } from 'ui/index_patterns';
import { IndexPatternTitle } from '../../../../../common/types/kibana';
import { ML_JOB_AGGREGATION } from '../../../../../common/constants/aggregation_types';
+import { ES_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/common';
import { Job, Datafeed, Detector, JobId, DatafeedId, BucketSpan } from './configs';
import { Aggregation, Field } from '../../../../../common/types/fields';
import { createEmptyJob, createEmptyDatafeed } from './util/default_configs';
@@ -33,6 +34,7 @@ export class JobCreator {
protected _subscribers: ProgressSubscriber[] = [];
protected _aggs: Aggregation[] = [];
protected _fields: Field[] = [];
+ protected _scriptFields: Field[] = [];
protected _sparseData: boolean = false;
private _stopAllRefreshPolls: {
stop: boolean;
@@ -413,6 +415,14 @@ export class JobCreator {
}
}
+ public get indices(): string[] {
+ return this._datafeed_config.indices;
+ }
+
+ public get scriptFields(): Field[] {
+ return this._scriptFields;
+ }
+
public get subscribers(): ProgressSubscriber[] {
return this._subscribers;
}
@@ -549,5 +559,16 @@ export class JobCreator {
this.useDedicatedIndex = true;
}
this._sparseData = isSparseDataJob(job, datafeed);
+
+ if (this._datafeed_config.script_fields !== undefined) {
+ this._scriptFields = Object.keys(this._datafeed_config.script_fields).map(f => ({
+ id: f,
+ name: f,
+ type: ES_FIELD_TYPES.KEYWORD,
+ aggregatable: true,
+ }));
+ } else {
+ this._scriptFields = [];
+ }
}
}
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/multi_metric_job_creator.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/multi_metric_job_creator.ts
index 39958b6d8f084..05a253a0962e9 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/multi_metric_job_creator.ts
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/multi_metric_job_creator.ts
@@ -141,7 +141,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, false);
+ const detectors = getRichDetectors(job, datafeed, this.scriptFields, false);
if (datafeed.aggregations !== undefined) {
// if we've converting from a single metric job,
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/population_job_creator.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/population_job_creator.ts
index 89611743a4404..ac7161422628d 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/population_job_creator.ts
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/population_job_creator.ts
@@ -126,7 +126,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, false);
+ const detectors = getRichDetectors(job, datafeed, this.scriptFields, false);
this.removeAllDetectors();
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/single_metric_job_creator.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/single_metric_job_creator.ts
index 5c5b7bfe712a3..aba3b08f330e8 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/single_metric_job_creator.ts
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/single_metric_job_creator.ts
@@ -186,7 +186,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, false);
+ const detectors = getRichDetectors(job, datafeed, this.scriptFields, false);
this.removeAllDetectors();
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/util/general.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/util/general.ts
index 15cbe5b7b8e5e..10a9260598dea 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/util/general.ts
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/util/general.ts
@@ -11,32 +11,99 @@ import {
ML_JOB_AGGREGATION,
SPARSE_DATA_AGGREGATIONS,
} from '../../../../../../common/constants/aggregation_types';
-import { EVENT_RATE_FIELD_ID, AggFieldPair } from '../../../../../../common/types/fields';
+import { MLCATEGORY } from '../../../../../../common/constants/field_types';
+import {
+ EVENT_RATE_FIELD_ID,
+ Field,
+ AggFieldPair,
+ mlCategory,
+} from '../../../../../../common/types/fields';
import { mlJobService } from '../../../../../services/job_service';
import { JobCreatorType, isMultiMetricJobCreator, isPopulationJobCreator } from '../';
import { CREATED_BY_LABEL, JOB_TYPE } from './constants';
+const getFieldByIdFactory = (scriptFields: 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;
+ }
+ }
+ return field;
+};
+
// populate the detectors with Field and Agg objects loaded from the job capabilities service
-export function getRichDetectors(job: Job, datafeed: Datafeed, advanced: boolean = false) {
+export function getRichDetectors(
+ job: Job,
+ datafeed: Datafeed,
+ scriptFields: Field[],
+ advanced: boolean = false
+) {
const detectors = advanced ? getDetectorsAdvanced(job, datafeed) : getDetectors(job, datafeed);
+
+ const getFieldById = getFieldByIdFactory(scriptFields);
+
return detectors.map(d => {
+ let field = null;
+ let byField = null;
+ let overField = null;
+ let partitionField = null;
+
+ if (d.field_name !== undefined) {
+ field = getFieldById(d.field_name);
+ }
+ if (d.by_field_name !== undefined) {
+ byField = getFieldById(d.by_field_name);
+ }
+ if (d.over_field_name !== undefined) {
+ overField = getFieldById(d.over_field_name);
+ }
+ if (d.partition_field_name !== undefined) {
+ partitionField = getFieldById(d.partition_field_name);
+ }
+
return {
agg: newJobCapsService.getAggById(d.function),
- field: d.field_name !== undefined ? newJobCapsService.getFieldById(d.field_name) : null,
- byField:
- d.by_field_name !== undefined ? newJobCapsService.getFieldById(d.by_field_name) : null,
- overField:
- d.over_field_name !== undefined ? newJobCapsService.getFieldById(d.over_field_name) : null,
- partitionField:
- d.partition_field_name !== undefined
- ? newJobCapsService.getFieldById(d.partition_field_name)
- : null,
+ field,
+ byField,
+ overField,
+ partitionField,
excludeFrequent: d.exclude_frequent || null,
description: d.detector_description || null,
};
});
}
+export function createFieldOptions(fields: Field[], filterOverride?: (f: Field) => boolean) {
+ const filter = filterOverride || (f => f.id !== EVENT_RATE_FIELD_ID);
+ return fields
+ .filter(filter)
+ .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 createMlcategoryFieldOption(categorizationFieldName: string | null) {
+ if (categorizationFieldName === null) {
+ return [];
+ }
+ return [
+ {
+ label: MLCATEGORY,
+ },
+ ];
+}
+
function getDetectorsAdvanced(job: Job, datafeed: Datafeed) {
return processFieldlessAggs(job.analysis_config.detectors);
}
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/common/json_editor_flyout/json_editor_flyout.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/common/json_editor_flyout/json_editor_flyout.tsx
index 8a14a92f86843..cf26d6f532d0d 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/common/json_editor_flyout/json_editor_flyout.tsx
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/common/json_editor_flyout/json_editor_flyout.tsx
@@ -18,7 +18,7 @@ import {
EuiFlyoutBody,
EuiSpacer,
} from '@elastic/eui';
-import { Job, Datafeed } from '../../../../common/job_creator/configs';
+import { Datafeed } from '../../../../common/job_creator/configs';
import { MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor';
import { isValidJson } from '../../../../../../../common/util/validation_utils';
import { JobCreatorContext } from '../../job_creator_context';
@@ -52,26 +52,32 @@ export const JsonEditorFlyout: FC = ({ isDisabled, jobEditorMode, datafee
const editJsonMode =
jobEditorMode === EDITOR_MODE.HIDDEN || datafeedEditorMode === EDITOR_MODE.HIDDEN;
const flyOutSize = editJsonMode ? 'm' : 'l';
+ const readOnlyMode =
+ jobEditorMode === EDITOR_MODE.READONLY && datafeedEditorMode === EDITOR_MODE.READONLY;
function toggleJsonFlyout() {
setSaveable(false);
setShowJsonFlyout(!showJsonFlyout);
}
- function updateSavable(json: string, originalConfig: Job | Datafeed) {
+ function onJobChange(json: string) {
+ setJobConfigString(json);
const valid = isValidJson(json);
setSaveable(valid);
}
- function onJobChange(json: string) {
- setJobConfigString(json);
-
- updateSavable(json, jobCreator.jobConfig);
- }
function onDatafeedChange(json: string) {
setDatafeedConfigString(json);
-
- updateSavable(json, jobCreator.datafeedConfig);
+ let valid = isValidJson(json);
+ if (valid) {
+ // ensure that the user hasn't altered the indices list in the json.
+ const { indices }: Datafeed = JSON.parse(json);
+ const originalIndices = jobCreator.indices.sort();
+ valid =
+ originalIndices.length === indices.length &&
+ originalIndices.every((value, index) => value === indices[index]);
+ }
+ setSaveable(valid);
}
function onSave() {
@@ -130,14 +136,16 @@ export const JsonEditorFlyout: FC = ({ isDisabled, jobEditorMode, datafee
/>
-
-
-
-
-
+ {readOnlyMode === false && (
+
+
+
+
+
+ )}
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/datafeed_step/components/time_field/time_field_select.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/datafeed_step/components/time_field/time_field_select.tsx
index 26508e13e7157..25e462dfa2286 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/datafeed_step/components/time_field/time_field_select.tsx
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/datafeed_step/components/time_field/time_field_select.tsx
@@ -9,6 +9,7 @@ import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui';
import { Field, EVENT_RATE_FIELD_ID } from '../../../../../../../../common/types/fields';
import { ES_FIELD_TYPES } from '../../../../../../../../../../../../src/plugins/data/public';
+import { createFieldOptions } from '../../../../../common/job_creator/util/general';
interface Props {
fields: Field[];
@@ -17,12 +18,10 @@ interface Props {
}
export const TimeFieldSelect: FC = ({ fields, changeHandler, selectedField }) => {
- const options: EuiComboBoxOptionProps[] = fields
- .filter(f => f.id !== EVENT_RATE_FIELD_ID && f.type === ES_FIELD_TYPES.DATE)
- .map(f => ({
- label: f.name,
- }))
- .sort((a, b) => a.label.localeCompare(b.label));
+ const options: EuiComboBoxOptionProps[] = createFieldOptions(
+ fields,
+ f => f.id !== EVENT_RATE_FIELD_ID && f.type === ES_FIELD_TYPES.DATE
+ );
const selection: EuiComboBoxOptionProps[] = [
{
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx
index b60239dcddd27..fb1f71f0d298a 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx
@@ -15,16 +15,20 @@ import {
EuiTextArea,
} from '@elastic/eui';
import { JobCreatorContext } from '../../../job_creator_context';
-import { AdvancedJobCreator, JobCreatorType } from '../../../../../common/job_creator';
+import { AdvancedJobCreator } from '../../../../../common/job_creator';
+import {
+ createFieldOptions,
+ createScriptFieldOptions,
+ createMlcategoryFieldOption,
+} from '../../../../../common/job_creator/util/general';
import {
Field,
Aggregation,
EVENT_RATE_FIELD_ID,
+ mlCategory,
} from '../../../../../../../../common/types/fields';
import { RichDetector } from '../../../../../common/job_creator/advanced_job_creator';
-import { ES_FIELD_TYPES } from '../../../../../../../../../../../../src/plugins/data/public';
import { ModalWrapper } from './modal_wrapper';
-import { MLCATEGORY } from '../../../../../../../../common/constants/field_types';
import { detectorToString } from '../../../../../../../util/string_utils';
import { createBasicDetector } from '../../../../../common/job_creator/util/default_configs';
@@ -55,13 +59,6 @@ const emptyOption: EuiComboBoxOptionProps = {
label: '',
};
-const mlCategory: Field = {
- id: MLCATEGORY,
- name: MLCATEGORY,
- type: ES_FIELD_TYPES.KEYWORD,
- aggregatable: false,
-};
-
const excludeFrequentOptions: EuiComboBoxOptionProps[] = [{ label: 'all' }, { label: 'none' }];
export const AdvancedDetectorModal: FC = ({
@@ -91,19 +88,28 @@ export const AdvancedDetectorModal: FC = ({
const [fieldOptionEnabled, setFieldOptionEnabled] = useState(true);
const { descriptionPlaceholder, setDescriptionPlaceholder } = useDetectorPlaceholder(detector);
- // list of aggregation combobox options. filtering out any aggs with no fields.
+ const usingScriptFields = jobCreator.scriptFields.length > 0;
+ // list of aggregation combobox options.
+
const aggOptions: EuiComboBoxOptionProps[] = aggs
- .filter(a => a.fields !== undefined && a.fields.length)
+ .filter(agg => filterAggs(agg, usingScriptFields))
.map(createAggOption);
// fields available for the selected agg
- const { currentFieldOptions, setCurrentFieldOptions } = useCurrentFieldOptions(detector.agg);
+ const { currentFieldOptions, setCurrentFieldOptions } = useCurrentFieldOptions(
+ detector.agg,
+ jobCreator.scriptFields
+ );
- const allFieldOptions: EuiComboBoxOptionProps[] = fields
- .filter(f => f.id !== EVENT_RATE_FIELD_ID)
- .map(createFieldOption);
+ const allFieldOptions: EuiComboBoxOptionProps[] = [
+ ...createFieldOptions(fields),
+ ...createScriptFieldOptions(jobCreator.scriptFields),
+ ];
- const splitFieldOptions = [...allFieldOptions, ...createMlcategoryField(jobCreator)];
+ const splitFieldOptions = [
+ ...allFieldOptions,
+ ...createMlcategoryFieldOption(jobCreator.categorizationFieldName),
+ ];
const eventRateField = fields.find(f => f.id === EVENT_RATE_FIELD_ID);
@@ -120,7 +126,9 @@ export const AdvancedDetectorModal: FC = ({
if (title === mlCategory.id) {
return mlCategory;
}
- return fields.find(a => a.id === title) || null;
+ return (
+ fields.find(f => f.id === title) || jobCreator.scriptFields.find(f => f.id === title) || null
+ );
}
useEffect(() => {
@@ -306,6 +314,13 @@ function createAggOption(agg: Aggregation | null): EuiComboBoxOptionProps {
};
}
+// get list of aggregations, filtering out any aggs with no fields,
+// unless script fields are being used, in which case list all fields, as it's not possible
+// to determine the type of a script field and so all aggs should be available.
+function filterAggs(agg: Aggregation, usingScriptFields: boolean) {
+ return agg.fields !== undefined && (usingScriptFields || agg.fields.length);
+}
+
function createFieldOption(field: Field | null): EuiComboBoxOptionProps {
if (field === null) {
return emptyOption;
@@ -331,17 +346,6 @@ function isFieldlessAgg(agg: Aggregation) {
return agg.fields && agg.fields.length === 1 && agg.fields[0].id === EVENT_RATE_FIELD_ID;
}
-function createMlcategoryField(jobCreator: JobCreatorType): EuiComboBoxOptionProps[] {
- if (jobCreator.categorizationFieldName === null) {
- return [];
- }
- return [
- {
- label: MLCATEGORY,
- },
- ];
-}
-
function useDetectorPlaceholder(detector: RichDetector) {
const [descriptionPlaceholder, setDescriptionPlaceholderString] = useState(
createDefaultDescription(detector)
@@ -355,22 +359,21 @@ function useDetectorPlaceholder(detector: RichDetector) {
}
// creates list of combobox options based on an aggregation's field list
-function createFieldOptionList(agg: Aggregation | null) {
- return (agg !== null && agg.fields !== undefined ? agg.fields : [])
- .filter(f => f.id !== EVENT_RATE_FIELD_ID)
- .map(createFieldOption);
+function createFieldOptionsFromAgg(agg: Aggregation | null) {
+ return createFieldOptions(agg !== null && agg.fields !== undefined ? agg.fields : []);
}
// custom hook for storing combobox options based on an aggregation field list
-function useCurrentFieldOptions(aggregation: Aggregation | null) {
+function useCurrentFieldOptions(aggregation: Aggregation | null, scriptFields: Field[]) {
const [currentFieldOptions, setCurrentFieldOptions] = useState(
- createFieldOptionList(aggregation)
+ createFieldOptionsFromAgg(aggregation)
);
+ const scriptFieldOptions = createScriptFieldOptions(scriptFields);
return {
currentFieldOptions,
setCurrentFieldOptions: (agg: Aggregation | null) =>
- setCurrentFieldOptions(createFieldOptionList(agg)),
+ setCurrentFieldOptions([...createFieldOptionsFromAgg(agg), ...scriptFieldOptions]),
};
}
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx
index 226ce20c72da7..f9d99f7d0f4e0 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx
@@ -4,11 +4,16 @@
* 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, EVENT_RATE_FIELD_ID } from '../../../../../../../../common/types/fields';
import { ES_FIELD_TYPES } from '../../../../../../../../../../../../src/plugins/data/public';
+import {
+ createFieldOptions,
+ createScriptFieldOptions,
+} from '../../../../../common/job_creator/util/general';
interface Props {
fields: Field[];
@@ -17,12 +22,14 @@ interface Props {
}
export const CategorizationFieldSelect: FC = ({ fields, changeHandler, selectedField }) => {
- const options: EuiComboBoxOptionProps[] = fields
- .filter(f => f.id !== EVENT_RATE_FIELD_ID && f.type === ES_FIELD_TYPES.KEYWORD)
- .map(f => ({
- label: f.name,
- }))
- .sort((a, b) => a.label.localeCompare(b.label));
+ const { jobCreator } = useContext(JobCreatorContext);
+ const options: EuiComboBoxOptionProps[] = [
+ ...createFieldOptions(
+ fields,
+ f => f.id !== EVENT_RATE_FIELD_ID && f.type === ES_FIELD_TYPES.KEYWORD
+ ),
+ ...createScriptFieldOptions(jobCreator.scriptFields),
+ ];
const selection: EuiComboBoxOptionProps[] = [
{
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers_select.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers_select.tsx
index b1f9c6dbcf555..293202415ced0 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers_select.tsx
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers_select.tsx
@@ -8,8 +8,12 @@ import React, { FC, useContext } from 'react';
import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui';
import { JobCreatorContext } from '../../../job_creator_context';
-import { Field, EVENT_RATE_FIELD_ID } from '../../../../../../../../common/types/fields';
-import { MLCATEGORY } from '../../../../../../../../common/constants/field_types';
+import { Field } from '../../../../../../../../common/types/fields';
+import {
+ createFieldOptions,
+ createScriptFieldOptions,
+ createMlcategoryFieldOption,
+} from '../../../../../common/job_creator/util/general';
interface Props {
fields: Field[];
@@ -19,18 +23,11 @@ interface Props {
export const InfluencersSelect: FC = ({ fields, changeHandler, selectedInfluencers }) => {
const { jobCreator } = useContext(JobCreatorContext);
- const options: EuiComboBoxOptionProps[] = fields
- .filter(f => f.id !== EVENT_RATE_FIELD_ID)
- .map(f => ({
- label: f.name,
- }))
- .sort((a, b) => a.label.localeCompare(b.label));
-
- if (jobCreator.categorizationFieldName !== null) {
- options.push({
- label: MLCATEGORY,
- });
- }
+ const options: EuiComboBoxOptionProps[] = [
+ ...createFieldOptions(fields),
+ ...createScriptFieldOptions(jobCreator.scriptFields),
+ ...createMlcategoryFieldOption(jobCreator.categorizationFieldName),
+ ];
const selection: EuiComboBoxOptionProps[] = selectedInfluencers.map(i => ({ label: i }));
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx
index d22d2ee111805..6bf510a70bfcb 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx
@@ -4,10 +4,15 @@
* 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 { Field, EVENT_RATE_FIELD_ID } from '../../../../../../../../common/types/fields';
+import { JobCreatorContext } from '../../../job_creator_context';
+import { Field } from '../../../../../../../../common/types/fields';
+import {
+ createFieldOptions,
+ createScriptFieldOptions,
+} from '../../../../../common/job_creator/util/general';
interface Props {
fields: Field[];
@@ -16,12 +21,11 @@ interface Props {
}
export const SummaryCountFieldSelect: FC = ({ fields, changeHandler, selectedField }) => {
- const options: EuiComboBoxOptionProps[] = fields
- .filter(f => f.id !== EVENT_RATE_FIELD_ID)
- .map(f => ({
- label: f.name,
- }))
- .sort((a, b) => a.label.localeCompare(b.label));
+ const { jobCreator } = useContext(JobCreatorContext);
+ const options: EuiComboBoxOptionProps[] = [
+ ...createFieldOptions(fields),
+ ...createScriptFieldOptions(jobCreator.scriptFields),
+ ];
const selection: EuiComboBoxOptionProps[] = [
{
diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/_timeseriesexplorer.scss b/x-pack/legacy/plugins/ml/public/timeseriesexplorer/_timeseriesexplorer.scss
index aa172cf000d5f..0dd32ceabdb05 100644
--- a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/_timeseriesexplorer.scss
+++ b/x-pack/legacy/plugins/ml/public/timeseriesexplorer/_timeseriesexplorer.scss
@@ -26,6 +26,10 @@
}
}
+ .single-metric-request-callout {
+ margin: 0 $euiSize;
+ }
+
.results-container {
padding: $euiSize;
diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/entity_control/entity_control.js b/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/entity_control/entity_control.js
index 0e82002904d51..7fb741096e32b 100644
--- a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/entity_control/entity_control.js
+++ b/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/entity_control/entity_control.js
@@ -6,12 +6,13 @@
import PropTypes from 'prop-types';
import React from 'react';
-import { injectI18n } from '@kbn/i18n/react';
+import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
import {
EuiComboBox,
EuiFlexItem,
EuiFormRow,
+ EuiToolTip,
} from '@elastic/eui';
function getEntityControlOptions(entity) {
@@ -29,6 +30,7 @@ export const EntityControl = injectI18n(
static propTypes = {
entity: PropTypes.object.isRequired,
entityFieldValueChanged: PropTypes.func.isRequired,
+ forceSelection: PropTypes.bool.isRequired,
};
state = {
@@ -40,7 +42,7 @@ export const EntityControl = injectI18n(
}
componentDidUpdate() {
- const { entity } = this.props;
+ const { entity, forceSelection } = this.props;
const { selectedOptions } = this.state;
const fieldValue = entity.fieldValue;
@@ -57,6 +59,10 @@ export const EntityControl = injectI18n(
selectedOptions: undefined
});
}
+
+ if (forceSelection && this.inputRef) {
+ this.inputRef.focus();
+ }
}
onChange = (selectedOptions) => {
@@ -70,25 +76,45 @@ export const EntityControl = injectI18n(
};
render() {
- const { entity, intl } = this.props;
+ const { entity, intl, forceSelection } = this.props;
const { selectedOptions } = this.state;
const options = getEntityControlOptions(entity);
+ const control = ( {
+ if (input) {
+ this.inputRef = input;
+ }
+ }}
+ style={{ minWidth: '300px' }}
+ placeholder={intl.formatMessage({
+ id: 'xpack.ml.timeSeriesExplorer.enterValuePlaceholder',
+ defaultMessage: 'Enter value'
+ })}
+ singleSelection={{ asPlainText: true }}
+ options={options}
+ selectedOptions={selectedOptions}
+ onChange={this.onChange}
+ isClearable={false}
+ />);
+
+ const selectMessage = ();
+
return (
-
-
+
+ {control}
+
);
diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.js b/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.js
index d335baa3a56d8..802772e55078c 100644
--- a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.js
+++ b/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.js
@@ -16,6 +16,7 @@ import PropTypes from 'prop-types';
import React, { createRef, Fragment } from 'react';
import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiCheckbox,
@@ -26,6 +27,7 @@ import {
EuiSelect,
EuiSpacer,
EuiText,
+ EuiCallOut,
} from '@elastic/eui';
import chrome from 'ui/chrome';
@@ -161,6 +163,8 @@ export class TimeSeriesExplorer extends React.Component {
subscriptions = new Subscription();
+ _criteriaFields = null;
+
constructor(props) {
super(props);
const { jobSelectService, unsubscribeFromGlobalState } = jobSelectServiceFactory(props.globalState);
@@ -216,17 +220,36 @@ export class TimeSeriesExplorer extends React.Component {
tableFilter = (field, value, operator) => {
const { entities } = this.state;
+ const entity = entities.find(({ fieldName }) => fieldName === field);
- const entity = find(entities, { fieldName: field });
- if (entity !== undefined) {
- if (operator === '+' && entity.fieldValue !== value) {
- entity.fieldValue = value;
- this.saveSeriesPropertiesAndRefresh();
- } else if (operator === '-' && entity.fieldValue === value) {
- entity.fieldValue = '';
- this.saveSeriesPropertiesAndRefresh();
- }
+ if (entity === undefined) {
+ return;
+ }
+
+ const { appStateHandler } = this.props;
+
+ let resultValue = '';
+ if (operator === '+' && entity.fieldValue !== value) {
+ resultValue = value;
+ } else if (operator === '-' && entity.fieldValue === value) {
+ resultValue = '';
+ } else {
+ return;
}
+
+ const resultEntities = {
+ ...entities.reduce((appStateEntities, appStateEntity) => {
+ appStateEntities[appStateEntity.fieldName] = appStateEntity.fieldValue;
+ return appStateEntities;
+ }, {}),
+ [entity.fieldName]: resultValue,
+ };
+
+ appStateHandler(APP_STATE_ACTION.SET_ENTITIES, resultEntities);
+
+ this.updateControlsForDetector(() => {
+ this.refresh();
+ });
}
contextChartSelectedInitCallDone = false;
@@ -296,7 +319,6 @@ export class TimeSeriesExplorer extends React.Component {
const searchBounds = getBoundsRoundedToInterval(bounds, focusAggregationInterval, false);
const {
- criteriaFields,
detectorId,
entities,
modelPlotEnabled,
@@ -310,7 +332,7 @@ export class TimeSeriesExplorer extends React.Component {
});
getFocusData(
- criteriaFields,
+ this._criteriaFields,
+detectorId,
focusAggregationInterval,
appStateHandler(APP_STATE_ACTION.GET_FORECAST_ID),
@@ -357,11 +379,11 @@ export class TimeSeriesExplorer extends React.Component {
loadAnomaliesTableData = (earliestMs, latestMs) => {
const { dateFormatTz } = this.props;
- const { criteriaFields, selectedJob } = this.state;
+ const { selectedJob } = this.state;
ml.results.getAnomaliesTableData(
[selectedJob.job_id],
- criteriaFields,
+ this._criteriaFields,
[],
interval$.getValue().val,
severity$.getValue().val,
@@ -430,16 +452,17 @@ export class TimeSeriesExplorer extends React.Component {
this.setState({
entities: entities.map((entity) => {
- if (firstRec.partition_field_name === entity.fieldName) {
- entity.fieldValues = chain(resp.records).pluck('partition_field_value').uniq().value();
+ const newEntity = { ...entity };
+ if (firstRec.partition_field_name === newEntity.fieldName) {
+ newEntity.fieldValues = chain(resp.records).pluck('partition_field_value').uniq().value();
}
- if (firstRec.over_field_name === entity.fieldName) {
- entity.fieldValues = chain(resp.records).pluck('over_field_value').uniq().value();
+ if (firstRec.over_field_name === newEntity.fieldName) {
+ newEntity.fieldValues = chain(resp.records).pluck('over_field_value').uniq().value();
}
- if (firstRec.by_field_name === entity.fieldName) {
- entity.fieldValues = chain(resp.records).pluck('by_field_value').uniq().value();
+ if (firstRec.by_field_name === newEntity.fieldName) {
+ newEntity.fieldValues = chain(resp.records).pluck('by_field_value').uniq().value();
}
- return entity;
+ return newEntity;
})
}, callback);
}
@@ -582,12 +605,7 @@ export class TimeSeriesExplorer extends React.Component {
}
};
- // Only filter on the entity if the field has a value.
const nonBlankEntities = filter(currentEntities, (entity) => { return entity.fieldValue.length > 0; });
- stateUpdate.criteriaFields = [{
- 'fieldName': 'detector_index',
- 'fieldValue': +currentDetectorId }
- ].concat(nonBlankEntities);
if (modelPlotEnabled === false &&
isSourceDataChartableForDetector(selectedJob, detectorIndex) === false &&
@@ -641,7 +659,7 @@ export class TimeSeriesExplorer extends React.Component {
// across full time range for use in the swimlane.
mlResultsService.getRecordMaxScoreByTime(
selectedJob.job_id,
- stateUpdate.criteriaFields,
+ this._criteriaFields,
searchBounds.min.valueOf(),
searchBounds.max.valueOf(),
stateUpdate.contextAggregationInterval.expression
@@ -698,6 +716,10 @@ export class TimeSeriesExplorer extends React.Component {
});
}
+ /**
+ * Updates local state of detector related controls from the global state.
+ * @param callback to invoke after a state update.
+ */
updateControlsForDetector = (callback = () => {}) => {
const { appStateHandler } = this.props;
const { detectorId, selectedJob } = this.state;
@@ -729,7 +751,26 @@ export class TimeSeriesExplorer extends React.Component {
entities.push({ fieldName: byFieldName, fieldValue: byFieldValue });
}
+ this.updateCriteriaFields(detectorIndex, entities);
+
this.setState({ entities }, callback);
+ };
+
+ /**
+ * Updates criteria fields for API calls, e.g. getAnomaliesTableData
+ * @param detectorIndex
+ * @param entities
+ */
+ updateCriteriaFields(detectorIndex, entities) {
+ // Only filter on the entity if the field has a value.
+ const nonBlankEntities = filter(entities, (entity) => { return entity.fieldValue.length > 0; });
+ this._criteriaFields = [
+ {
+ fieldName: 'detector_index',
+ fieldValue: detectorIndex
+ },
+ ...nonBlankEntities
+ ];
}
loadForJobId(jobId, jobs) {
@@ -988,6 +1029,11 @@ export class TimeSeriesExplorer extends React.Component {
zoomToFocusLoaded,
} = this.state;
+ const fieldNamesWithEmptyValues = entities
+ .filter(({ fieldValue }) => !fieldValue)
+ .map(({ fieldName }) => fieldName);
+ const arePartitioningFieldsProvided = fieldNamesWithEmptyValues.length === 0;
+
const chartProps = {
modelPlotEnabled,
contextChartData,
@@ -1050,9 +1096,32 @@ export class TimeSeriesExplorer extends React.Component {
this.previousShowForecast = showForecast;
this.previousShowModelBounds = showModelBounds;
+ /**
+ * Indicates if any of the previous controls is empty.
+ * @type {boolean}
+ */
+ let hasEmptyFieldValues = false;
+
return (
-
+
+ {fieldNamesWithEmptyValues.length > 0 && }
+ color="warning"
+ iconType="help"
+ size="s"
+ />}
+
@@ -1070,14 +1139,18 @@ export class TimeSeriesExplorer extends React.Component {
{entities.map((entity) => {
const entityKey = `${entity.fieldName}`;
+ const forceSelection = !hasEmptyFieldValues && !entity.fieldValue;
+ hasEmptyFieldValues = !hasEmptyFieldValues && forceSelection;
return (
);
})}
+ {arePartitioningFieldsProvided &&
-
+ }
@@ -1100,12 +1173,14 @@ export class TimeSeriesExplorer extends React.Component {
/>
)}
- {(jobs.length > 0 && loading === false && hasResults === false) && (
+ {(arePartitioningFieldsProvided && jobs.length > 0 && loading === false && hasResults === false) && (
)}
- {(jobs.length > 0 && loading === false && hasResults === true) && (
+ {(arePartitioningFieldsProvided && jobs.length > 0 && loading === false && hasResults === true) && (
+
+
{i18n.translate('xpack.ml.timeSeriesExplorer.singleTimeSeriesAnalysisTitle', {
defaultMessage: 'Single time series analysis of {functionLabel}',
diff --git a/x-pack/legacy/plugins/siem/public/store/network/reducer.ts b/x-pack/legacy/plugins/siem/public/store/network/reducer.ts
index b2192a92fdf1c..88adc118d51dc 100644
--- a/x-pack/legacy/plugins/siem/public/store/network/reducer.ts
+++ b/x-pack/legacy/plugins/siem/public/store/network/reducer.ts
@@ -45,7 +45,7 @@ export const initialNetworkState: NetworkState = {
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
limit: DEFAULT_TABLE_LIMIT,
sort: {
- field: NetworkTopTablesFields.bytes_out,
+ field: NetworkTopTablesFields.bytes_in,
direction: Direction.desc,
},
},
@@ -78,7 +78,7 @@ export const initialNetworkState: NetworkState = {
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
limit: DEFAULT_TABLE_LIMIT,
sort: {
- field: NetworkTopTablesFields.bytes_out,
+ field: NetworkTopTablesFields.bytes_in,
direction: Direction.desc,
},
},
@@ -98,7 +98,7 @@ export const initialNetworkState: NetworkState = {
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
limit: DEFAULT_TABLE_LIMIT,
sort: {
- field: NetworkTopTablesFields.bytes_out,
+ field: NetworkTopTablesFields.bytes_in,
direction: Direction.desc,
},
},
@@ -114,7 +114,7 @@ export const initialNetworkState: NetworkState = {
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
limit: DEFAULT_TABLE_LIMIT,
sort: {
- field: NetworkTopTablesFields.bytes_out,
+ field: NetworkTopTablesFields.bytes_in,
direction: Direction.desc,
},
},
diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/home.test.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/home.test.ts
index 7f4860e74bafe..e2782a1358251 100644
--- a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/home.test.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/home.test.ts
@@ -400,9 +400,7 @@ describe.skip('', () => {
test('should invite the user to first register a repository', () => {
const { find, exists } = testBed;
- expect(find('emptyPrompt.title').text()).toBe(
- `You don't have any snapshots or repositories yet`
- );
+ expect(find('emptyPrompt.title').text()).toBe('Start by registering a repository');
expect(exists('emptyPrompt.registerRepositoryButton')).toBe(true);
});
});
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_list.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_list.tsx
index a1688b8e35486..dfcf75b5b89a0 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_list.tsx
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_list.tsx
@@ -113,7 +113,7 @@ export const PolicyList: React.FunctionComponent
}
@@ -122,7 +122,7 @@ export const PolicyList: React.FunctionComponent
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_list.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_list.tsx
index fb8eb7b771b87..5e3250f082fce 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_list.tsx
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_list.tsx
@@ -99,7 +99,7 @@ export const RepositoryList: React.FunctionComponent
}
@@ -108,7 +108,7 @@ export const RepositoryList: React.FunctionComponent
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/restore_list/restore_list.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/restore_list/restore_list.tsx
index dfbfcf27e6fab..ec4b8d9f19fbb 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/restore_list/restore_list.tsx
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/restore_list/restore_list.tsx
@@ -98,7 +98,7 @@ export const RestoreList: React.FunctionComponent = () => {
}
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_list.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_list.tsx
index e10a528d8a961..642a12411e6f3 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_list.tsx
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_list.tsx
@@ -168,7 +168,7 @@ export const SnapshotList: React.FunctionComponent
}
@@ -177,7 +177,7 @@ export const SnapshotList: React.FunctionComponent
diff --git a/x-pack/legacy/plugins/transform/common/constants.ts b/x-pack/legacy/plugins/transform/common/constants.ts
index 3c6224c96d6f2..c85408d3c5ce6 100644
--- a/x-pack/legacy/plugins/transform/common/constants.ts
+++ b/x-pack/legacy/plugins/transform/common/constants.ts
@@ -24,40 +24,54 @@ export const PLUGIN = {
export const API_BASE_PATH = '/api/transform/';
-// Current df_admin permission requirements:
-// 1. `read` on source index
-// 2. `all` on source index to create and start transform
-// 3. `all` on dest index (could be less tbd)
-// 3. `monitor` cluster privilege
-// 4. builtin `data_frame_transforms_admin`
-// 5. builtin `kibana_user`
-// 6. builtin `data_frame_transforms_user` (although this is probably included in the admin)
+// In order to create a transform, the API requires the following privileges:
+// - transform_admin (builtin)
+// - cluster privileges: manage_transform
+// - index privileges:
+// - indices: .transform-notifications-*, .data-frame-notifications-*, .transform-notifications-read
+// - privileges: view_index_metadata, read
+// - transform_user (builtin)
+// - cluster privileges: monitor_transform
+// - index privileges:
+// - indices: .transform-notifications-*, .data-frame-notifications-*, .transform-notifications-read
+// - privileges: view_index_metadata, read
+// - source index: read, view_index_metadata (can be applied to a pattern e.g. farequote-*)
+// - dest index: index, create_index (can be applied to a pattern e.g. df-*)
+//
+// In the UI additional privileges are required:
+// - kibana_user (builtin)
+// - dest index: monitor (applied to df-*)
+// - cluster: monitor
+//
+// Note that users with kibana_user can see all Kibana index patterns and saved searches
+// in the source selection modal when creating a transform, but the wizard will trigger
+// error callouts when there are no sufficient privileges to read the actual source indices.
export const APP_CLUSTER_PRIVILEGES = [
- 'cluster:monitor/data_frame/get',
- 'cluster:monitor/data_frame/stats/get',
- 'cluster:admin/data_frame/delete',
- 'cluster:admin/data_frame/preview',
- 'cluster:admin/data_frame/put',
- 'cluster:admin/data_frame/start',
- 'cluster:admin/data_frame/start_task',
- 'cluster:admin/data_frame/stop',
+ 'cluster:monitor/transform/get',
+ 'cluster:monitor/transform/stats/get',
+ 'cluster:admin/transform/delete',
+ 'cluster:admin/transform/preview',
+ 'cluster:admin/transform/put',
+ 'cluster:admin/transform/start',
+ 'cluster:admin/transform/start_task',
+ 'cluster:admin/transform/stop',
];
// Equivalent of capabilities.canGetTransform
export const APP_GET_TRANSFORM_CLUSTER_PRIVILEGES = [
- 'cluster.cluster:monitor/data_frame/get',
- 'cluster.cluster:monitor/data_frame/stats/get',
+ 'cluster.cluster:monitor/transform/get',
+ 'cluster.cluster:monitor/transform/stats/get',
];
// Equivalent of capabilities.canGetTransform
export const APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES = [
- 'cluster.cluster:monitor/data_frame/get',
- 'cluster.cluster:monitor/data_frame/stats/get',
- 'cluster.cluster:admin/data_frame/preview',
- 'cluster.cluster:admin/data_frame/put',
- 'cluster.cluster:admin/data_frame/start',
- 'cluster.cluster:admin/data_frame/start_task',
+ 'cluster.cluster:monitor/transform/get',
+ 'cluster.cluster:monitor/transform/stats/get',
+ 'cluster.cluster:admin/transform/preview',
+ 'cluster.cluster:admin/transform/put',
+ 'cluster.cluster:admin/transform/start',
+ 'cluster.cluster:admin/transform/start_task',
];
export const APP_INDEX_PRIVILEGES = ['monitor'];
diff --git a/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.test.tsx b/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.test.tsx
index ffdb79a9d410b..2a0ce8ca08bec 100644
--- a/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.test.tsx
+++ b/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.test.tsx
@@ -25,6 +25,8 @@ describe('ToastNotificationText', () => {
'a text message that is longer than 140 characters. a text message that is longer than 140 characters. a text message that is longer than 140 characters. ',
};
const { container } = render();
- expect(container.textContent).toBe('View details');
+ expect(container.textContent).toBe(
+ 'a text message that is longer than 140 characters. a text message that is longer than 140 characters. a text message that is longer than 140 ...View details'
+ );
});
});
diff --git a/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.tsx b/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.tsx
index 15cb23415bb3a..28bb9b2687913 100644
--- a/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.tsx
+++ b/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.tsx
@@ -35,7 +35,11 @@ export const ToastNotificationText: FC<{ text: any }> = ({ text }) => {
return text.message;
}
- const formattedText = text.message ? text.message : JSON.stringify(text, null, 2);
+ const unformattedText = text.message ? text.message : text;
+ const formattedText = typeof unformattedText === 'object' ? JSON.stringify(text, null, 2) : text;
+ const previewText = `${formattedText.substring(0, 140)}${
+ formattedText.length > 140 ? ' ...' : ''
+ }`;
const openModal = () => {
const modal = npStart.core.overlays.openModal(
@@ -65,6 +69,7 @@ export const ToastNotificationText: FC<{ text: any }> = ({ text }) => {
return (
<>
+
{previewText}
{i18n.translate('xpack.transform.toastText.openModalButtonText', {
defaultMessage: 'View details',
diff --git a/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx b/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx
index 84c7d616c19b8..8060659c78b80 100644
--- a/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx
+++ b/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx
@@ -64,25 +64,25 @@ export const AuthorizationProvider = ({ privilegesEndpoint, children }: Props) =
const hasPrivilege = hasPrivilegeFactory(value.privileges);
value.capabilities.canGetTransform =
- hasPrivilege(['cluster', 'cluster:monitor/data_frame/get']) &&
- hasPrivilege(['cluster', 'cluster:monitor/data_frame/stats/get']);
+ hasPrivilege(['cluster', 'cluster:monitor/transform/get']) &&
+ hasPrivilege(['cluster', 'cluster:monitor/transform/stats/get']);
- value.capabilities.canCreateTransform = hasPrivilege(['cluster', 'cluster:admin/data_frame/put']);
+ value.capabilities.canCreateTransform = hasPrivilege(['cluster', 'cluster:admin/transform/put']);
value.capabilities.canDeleteTransform = hasPrivilege([
'cluster',
- 'cluster:admin/data_frame/delete',
+ 'cluster:admin/transform/delete',
]);
value.capabilities.canPreviewTransform = hasPrivilege([
'cluster',
- 'cluster:admin/data_frame/preview',
+ 'cluster:admin/transform/preview',
]);
value.capabilities.canStartStopTransform =
- hasPrivilege(['cluster', 'cluster:admin/data_frame/start']) &&
- hasPrivilege(['cluster', 'cluster:admin/data_frame/start_task']) &&
- hasPrivilege(['cluster', 'cluster:admin/data_frame/stop']);
+ hasPrivilege(['cluster', 'cluster:admin/transform/start']) &&
+ hasPrivilege(['cluster', 'cluster:admin/transform/start_task']) &&
+ hasPrivilege(['cluster', 'cluster:admin/transform/stop']);
return (
{children}
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx
index 27234af868fbd..504156b8b89ba 100644
--- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx
+++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx
@@ -15,6 +15,7 @@ import {
EuiButtonIcon,
EuiCallOut,
EuiCheckbox,
+ EuiCodeBlock,
EuiCopy,
EuiFlexGroup,
EuiFlexItem,
@@ -142,7 +143,9 @@ export const SourceIndexPreview: React.SFC = React.memo(({ cellClick, que
color="danger"
iconType="cross"
>
- {errorMessage}
+
+ {errorMessage}
+
);
@@ -164,7 +167,7 @@ export const SourceIndexPreview: React.SFC = React.memo(({ cellClick, que
{i18n.translate('xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutBody', {
defaultMessage:
- 'The query for the source index returned no results. Please make sure the index contains documents and your query is not too restrictive.',
+ 'The query for the source index returned no results. Please make sure you have sufficient permissions, the index contains documents and your query is not too restrictive.',
})}
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts
index 9d60e3550fd75..1f8f25d33e807 100644
--- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts
+++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts
@@ -32,6 +32,23 @@ export enum SOURCE_INDEX_STATUS {
ERROR,
}
+interface ErrorResponse {
+ error: {
+ body: string;
+ msg: string;
+ path: string;
+ query: any;
+ response: string;
+ statusCode: number;
+ };
+}
+
+const isErrorResponse = (arg: any): arg is ErrorResponse => {
+ return arg.error !== undefined;
+};
+
+type SourceIndexSearchResponse = ErrorResponse | SearchResponse;
+
export interface UseSourceIndexDataReturnType {
errorMessage: string;
status: SOURCE_INDEX_STATUS;
@@ -54,13 +71,17 @@ export const useSourceIndexData = (
setStatus(SOURCE_INDEX_STATUS.LOADING);
try {
- const resp: SearchResponse = await api.esSearch({
+ const resp: SourceIndexSearchResponse = await api.esSearch({
index: indexPattern.title,
size: SEARCH_SIZE,
// Instead of using the default query (`*`), fall back to a more efficient `match_all` query.
body: { query: isDefaultQuery(query) ? { match_all: {} } : query },
});
+ if (isErrorResponse(resp)) {
+ throw resp.error;
+ }
+
const docs = resp.hits.hits;
if (docs.length === 0) {
@@ -101,7 +122,7 @@ export const useSourceIndexData = (
if (e.message !== undefined) {
setErrorMessage(e.message);
} else {
- setErrorMessage(JSON.stringify(e));
+ setErrorMessage(JSON.stringify(e, null, 2));
}
setTableItems([]);
setStatus(SOURCE_INDEX_STATUS.ERROR);
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx
index 3f5ddc6238faa..47ce0f19f8f69 100644
--- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx
+++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx
@@ -88,7 +88,16 @@ export const StepCreateForm: SFC = React.memo(
setCreated(true);
try {
- await api.createTransform(transformId, transformConfig);
+ const resp = await api.createTransform(transformId, transformConfig);
+
+ if (resp.errors !== undefined) {
+ if (Array.isArray(resp.errors) && resp.errors.length === 1) {
+ throw resp.errors[0];
+ }
+
+ throw resp.errors;
+ }
+
toastNotifications.addSuccess(
i18n.translate('xpack.transform.stepCreateForm.createTransformSuccessMessage', {
defaultMessage: 'Request to create transform {transformId} acknowledged.',
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.tsx
index 3ac87f61d4c89..bbfc6b11d3619 100644
--- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.tsx
+++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.tsx
@@ -12,12 +12,12 @@ import { i18n } from '@kbn/i18n';
import {
EuiButtonIcon,
EuiCallOut,
+ EuiCodeBlock,
EuiCopy,
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiProgress,
- EuiText,
EuiTitle,
} from '@elastic/eui';
@@ -102,24 +102,11 @@ interface ErrorMessageProps {
message: string;
}
-const ErrorMessage: SFC = ({ message }) => {
- const error = JSON.parse(message);
-
- const statusCodeLabel = i18n.translate('xpack.transform.pivotPreview.statusCodeLabel', {
- defaultMessage: 'Status code',
- });
-
- return (
-
-
- {(error.message &&
- error.statusCode &&
- `${statusCodeLabel}: ${error.statusCode}\n${error.message}`) ||
- message}
-
-
- );
-};
+const ErrorMessage: SFC = ({ message }) => (
+
+ {message}
+
+);
interface PivotPreviewProps {
aggs: PivotAggsConfigDict;
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.ts b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.ts
index 7f7d06a7d18e6..17deb4db31990 100644
--- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.ts
+++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.ts
@@ -82,7 +82,7 @@ export const usePivotPreviewData = (
setPreviewMappings(resp.mappings);
setStatus(PIVOT_PREVIEW_STATUS.LOADED);
} catch (e) {
- setErrorMessage(JSON.stringify(e));
+ setErrorMessage(JSON.stringify(e, null, 2));
setPreviewData([]);
setPreviewMappings({ properties: {} });
setStatus(PIVOT_PREVIEW_STATUS.ERROR);
@@ -97,5 +97,6 @@ export const usePivotPreviewData = (
JSON.stringify(groupByArr),
JSON.stringify(query),
]);
+
return { errorMessage, status, previewData, previewMappings, previewRequest };
};
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/__snapshots__/transform_management_section.test.tsx.snap b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/__snapshots__/transform_management_section.test.tsx.snap
index df8002cb7145d..8348f93b32140 100644
--- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/__snapshots__/transform_management_section.test.tsx.snap
+++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/__snapshots__/transform_management_section.test.tsx.snap
@@ -4,8 +4,8 @@ exports[`Transform: Minimal initialization 1`] =
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 40a3592440031..4ff1459637889 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -6990,7 +6990,6 @@
"xpack.transform.transformList.stopActionName": "停止",
"xpack.transform.transformList.stopTransformSuccessMessage": "データフレームジョブ {transformId} が停止しました",
"xpack.transform.pivotPreview.copyClipboardTooltip": "ピボットプレビューの開発コンソールステートメントをクリップボードにコピーします。",
- "xpack.transform.pivotPreview.statusCodeLabel": "ステータスコード",
"xpack.transform.progress": "進捗",
"xpack.transform.sourceIndex": "ソースインデックス",
"xpack.transform.sourceIndexPreview.copyClipboardTooltip": "ソースインデックスプレビューの開発コンソールステートメントをクリップボードにコピーします。",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index c32636d20bf76..116a1f15a139b 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -7148,7 +7148,6 @@
"xpack.transform.transformList.stopActionName": "停止",
"xpack.transform.transformList.stopTransformSuccessMessage": "数据帧作业 {transformId} 停止成功。",
"xpack.transform.pivotPreview.copyClipboardTooltip": "将透视预览的开发控制台语句复制到剪贴板。",
- "xpack.transform.pivotPreview.statusCodeLabel": "状态代码",
"xpack.transform.progress": "进度",
"xpack.transform.sourceIndex": "源索引",
"xpack.transform.sourceIndexPreview.copyClipboardTooltip": "将源索引预览的开发控制台语句复制到剪贴板。",