diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md
index 04a0d871cab2d..3969a97fa7789 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md
@@ -7,7 +7,10 @@
Signature:
```typescript
-export declare function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, forceNow?: Date): import("../..").RangeFilter | undefined;
+export declare function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, options?: {
+ forceNow?: Date;
+ fieldName?: string;
+}): import("../..").RangeFilter | undefined;
```
## Parameters
@@ -16,7 +19,7 @@ export declare function getTime(indexPattern: IIndexPattern | undefined, timeRan
| --- | --- | --- |
| indexPattern | IIndexPattern | undefined
| |
| timeRange | TimeRange
| |
-| forceNow | Date
| |
+| options | {
forceNow?: Date;
fieldName?: string;
}
| |
Returns:
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md
new file mode 100644
index 0000000000000..c3998876c9712
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md
@@ -0,0 +1,15 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IIndexPattern](./kibana-plugin-plugins-data-public.iindexpattern.md) > [getTimeField](./kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md)
+
+## IIndexPattern.getTimeField() method
+
+Signature:
+
+```typescript
+getTimeField?(): IFieldType | undefined;
+```
+Returns:
+
+`IFieldType | undefined`
+
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md
index 1bbd6cf67f0ce..1cb89822eb605 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md
@@ -21,3 +21,9 @@ export interface IIndexPattern
| [title](./kibana-plugin-plugins-data-public.iindexpattern.title.md) | string
| |
| [type](./kibana-plugin-plugins-data-public.iindexpattern.type.md) | string
| |
+## Methods
+
+| Method | Description |
+| --- | --- |
+| [getTimeField()](./kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md) | |
+
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md
index 0fd82ffb2240c..e1df493143b73 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md
@@ -43,7 +43,7 @@
| [getEsPreference(uiSettings, sessionId)](./kibana-plugin-plugins-data-public.getespreference.md) | |
| [getQueryLog(uiSettings, storage, appName, language)](./kibana-plugin-plugins-data-public.getquerylog.md) | |
| [getSearchErrorType({ message })](./kibana-plugin-plugins-data-public.getsearcherrortype.md) | |
-| [getTime(indexPattern, timeRange, forceNow)](./kibana-plugin-plugins-data-public.gettime.md) | |
+| [getTime(indexPattern, timeRange, options)](./kibana-plugin-plugins-data-public.gettime.md) | |
| [plugin(initializerContext)](./kibana-plugin-plugins-data-public.plugin.md) | |
## Interfaces
diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts
index 698edbf9cd6a8..e21d27a70e02a 100644
--- a/src/plugins/data/common/index_patterns/types.ts
+++ b/src/plugins/data/common/index_patterns/types.ts
@@ -26,6 +26,7 @@ export interface IIndexPattern {
id?: string;
type?: string;
timeFieldName?: string;
+ getTimeField?(): IFieldType | undefined;
fieldFormatMap?: Record<
string,
{
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index 86560b3ccf7b1..91dea66f06a94 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -699,7 +699,10 @@ export function getSearchErrorType({ message }: Pick): "
// Warning: (ae-missing-release-tag) "getTime" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
-export function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, forceNow?: Date): import("../..").RangeFilter | undefined;
+export function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, options?: {
+ forceNow?: Date;
+ fieldName?: string;
+}): import("../..").RangeFilter | undefined;
// Warning: (ae-missing-release-tag) "IAggConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -842,6 +845,8 @@ export interface IIndexPattern {
// (undocumented)
fields: IFieldType[];
// (undocumented)
+ getTimeField?(): IFieldType | undefined;
+ // (undocumented)
id?: string;
// (undocumented)
timeFieldName?: string;
diff --git a/src/plugins/data/public/query/timefilter/get_time.test.ts b/src/plugins/data/public/query/timefilter/get_time.test.ts
index a8eb3a3fe8102..4dba157a6f554 100644
--- a/src/plugins/data/public/query/timefilter/get_time.test.ts
+++ b/src/plugins/data/public/query/timefilter/get_time.test.ts
@@ -51,5 +51,43 @@ describe('get_time', () => {
});
clock.restore();
});
+
+ test('build range filter for non-primary field', () => {
+ const clock = sinon.useFakeTimers(moment.utc([2000, 1, 1, 0, 0, 0, 0]).valueOf());
+
+ const filter = getTime(
+ {
+ id: 'test',
+ title: 'test',
+ timeFieldName: 'date',
+ fields: [
+ {
+ name: 'date',
+ type: 'date',
+ esTypes: ['date'],
+ aggregatable: true,
+ searchable: true,
+ filterable: true,
+ },
+ {
+ name: 'myCustomDate',
+ type: 'date',
+ esTypes: ['date'],
+ aggregatable: true,
+ searchable: true,
+ filterable: true,
+ },
+ ],
+ } as any,
+ { from: 'now-60y', to: 'now' },
+ { fieldName: 'myCustomDate' }
+ );
+ expect(filter!.range.myCustomDate).toEqual({
+ gte: '1940-02-01T00:00:00.000Z',
+ lte: '2000-02-01T00:00:00.000Z',
+ format: 'strict_date_optional_time',
+ });
+ clock.restore();
+ });
});
});
diff --git a/src/plugins/data/public/query/timefilter/get_time.ts b/src/plugins/data/public/query/timefilter/get_time.ts
index fa15406189041..9cdd25d3213ce 100644
--- a/src/plugins/data/public/query/timefilter/get_time.ts
+++ b/src/plugins/data/public/query/timefilter/get_time.ts
@@ -19,7 +19,7 @@
import dateMath from '@elastic/datemath';
import { IIndexPattern } from '../..';
-import { TimeRange, IFieldType, buildRangeFilter } from '../../../common';
+import { TimeRange, buildRangeFilter } from '../../../common';
interface CalculateBoundsOptions {
forceNow?: Date;
@@ -35,18 +35,27 @@ export function calculateBounds(timeRange: TimeRange, options: CalculateBoundsOp
export function getTime(
indexPattern: IIndexPattern | undefined,
timeRange: TimeRange,
+ options?: { forceNow?: Date; fieldName?: string }
+) {
+ return createTimeRangeFilter(
+ indexPattern,
+ timeRange,
+ options?.fieldName || indexPattern?.timeFieldName,
+ options?.forceNow
+ );
+}
+
+function createTimeRangeFilter(
+ indexPattern: IIndexPattern | undefined,
+ timeRange: TimeRange,
+ fieldName?: string,
forceNow?: Date
) {
if (!indexPattern) {
- // in CI, we sometimes seem to fail here.
return;
}
-
- const timefield: IFieldType | undefined = indexPattern.fields.find(
- field => field.name === indexPattern.timeFieldName
- );
-
- if (!timefield) {
+ const field = indexPattern.fields.find(f => f.name === (fieldName || indexPattern.timeFieldName));
+ if (!field) {
return;
}
@@ -55,7 +64,7 @@ export function getTime(
return;
}
return buildRangeFilter(
- timefield,
+ field,
{
...(bounds.min && { gte: bounds.min.toISOString() }),
...(bounds.max && { lte: bounds.max.toISOString() }),
diff --git a/src/plugins/data/public/query/timefilter/index.ts b/src/plugins/data/public/query/timefilter/index.ts
index a6260e782c12f..034af03842ab8 100644
--- a/src/plugins/data/public/query/timefilter/index.ts
+++ b/src/plugins/data/public/query/timefilter/index.ts
@@ -22,6 +22,6 @@ export { TimefilterService, TimefilterSetup } from './timefilter_service';
export * from './types';
export { Timefilter, TimefilterContract } from './timefilter';
export { TimeHistory, TimeHistoryContract } from './time_history';
-export { getTime } from './get_time';
+export { getTime, calculateBounds } from './get_time';
export { changeTimeFilter } from './lib/change_time_filter';
export { extractTimeFilter } from './lib/extract_time_filter';
diff --git a/src/plugins/data/public/query/timefilter/timefilter.ts b/src/plugins/data/public/query/timefilter/timefilter.ts
index 4fbdac47fb3b0..86ef69be572a9 100644
--- a/src/plugins/data/public/query/timefilter/timefilter.ts
+++ b/src/plugins/data/public/query/timefilter/timefilter.ts
@@ -164,7 +164,9 @@ export class Timefilter {
};
public createFilter = (indexPattern: IndexPattern, timeRange?: TimeRange) => {
- return getTime(indexPattern, timeRange ? timeRange : this._time, this.getForceNow());
+ return getTime(indexPattern, timeRange ? timeRange : this._time, {
+ forceNow: this.getForceNow(),
+ });
};
public getBounds(): TimeRangeBounds {
diff --git a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts b/src/plugins/data/public/search/aggs/buckets/date_histogram.ts
index 57f3aa85ad944..3ecdc17cb57f3 100644
--- a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts
+++ b/src/plugins/data/public/search/aggs/buckets/date_histogram.ts
@@ -45,7 +45,7 @@ const updateTimeBuckets = (
customBuckets?: IBucketDateHistogramAggConfig['buckets']
) => {
const bounds =
- agg.params.timeRange && agg.fieldIsTimeField()
+ agg.params.timeRange && (agg.fieldIsTimeField() || agg.params.interval === 'auto')
? timefilter.calculateBounds(agg.params.timeRange)
: undefined;
const buckets = customBuckets || agg.buckets;
diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts
index 087b83127079f..eec75b0841133 100644
--- a/src/plugins/data/public/search/expressions/esaggs.ts
+++ b/src/plugins/data/public/search/expressions/esaggs.ts
@@ -32,8 +32,15 @@ import { Adapters } from '../../../../../plugins/inspector/public';
import { IAggConfigs } from '../aggs';
import { ISearchSource } from '../search_source';
import { tabifyAggResponse } from '../tabify';
-import { Filter, Query, serializeFieldFormat, TimeRange } from '../../../common';
-import { FilterManager, getTime } from '../../query';
+import {
+ Filter,
+ Query,
+ serializeFieldFormat,
+ TimeRange,
+ IIndexPattern,
+ isRangeFilter,
+} from '../../../common';
+import { FilterManager, calculateBounds, getTime } from '../../query';
import { getSearchService, getQueryService, getIndexPatterns } from '../../services';
import { buildTabularInspectorData } from './build_tabular_inspector_data';
import { getRequestInspectorStats, getResponseInspectorStats, serializeAggConfig } from './utils';
@@ -42,6 +49,8 @@ export interface RequestHandlerParams {
searchSource: ISearchSource;
aggs: IAggConfigs;
timeRange?: TimeRange;
+ timeFields?: string[];
+ indexPattern?: IIndexPattern;
query?: Query;
filters?: Filter[];
forceFetch: boolean;
@@ -65,12 +74,15 @@ interface Arguments {
partialRows: boolean;
includeFormatHints: boolean;
aggConfigs: string;
+ timeFields?: string[];
}
const handleCourierRequest = async ({
searchSource,
aggs,
timeRange,
+ timeFields,
+ indexPattern,
query,
filters,
forceFetch,
@@ -111,9 +123,19 @@ const handleCourierRequest = async ({
return aggs.onSearchRequestStart(paramSearchSource, options);
});
- if (timeRange) {
+ // If timeFields have been specified, use the specified ones, otherwise use primary time field of index
+ // pattern if it's available.
+ const defaultTimeField = indexPattern?.getTimeField?.();
+ const defaultTimeFields = defaultTimeField ? [defaultTimeField.name] : [];
+ const allTimeFields = timeFields && timeFields.length > 0 ? timeFields : defaultTimeFields;
+
+ // If a timeRange has been specified and we had at least one timeField available, create range
+ // filters for that those time fields
+ if (timeRange && allTimeFields.length > 0) {
timeFilterSearchSource.setField('filter', () => {
- return getTime(searchSource.getField('index'), timeRange);
+ return allTimeFields
+ .map(fieldName => getTime(indexPattern, timeRange, { fieldName }))
+ .filter(isRangeFilter);
});
}
@@ -181,11 +203,13 @@ const handleCourierRequest = async ({
(searchSource as any).finalResponse = resp;
- const parsedTimeRange = timeRange ? getTime(aggs.indexPattern, timeRange) : null;
+ const parsedTimeRange = timeRange ? calculateBounds(timeRange) : null;
const tabifyParams = {
metricsAtAllLevels,
partialRows,
- timeRange: parsedTimeRange ? parsedTimeRange.range : undefined,
+ timeRange: parsedTimeRange
+ ? { from: parsedTimeRange.min, to: parsedTimeRange.max, timeFields: allTimeFields }
+ : undefined,
};
const tabifyCacheHash = calculateObjectHash({ tabifyAggs: aggs, ...tabifyParams });
@@ -242,6 +266,11 @@ export const esaggs = (): ExpressionFunctionDefinition {
const check = (aggResp: any, count: number, keys: string[]) => {
@@ -187,9 +192,9 @@ describe('Buckets wrapper', () => {
},
};
const timeRange = {
- gte: 150,
- lte: 350,
- name: 'date',
+ from: moment(150),
+ to: moment(350),
+ timeFields: ['date'],
};
const buckets = new TabifyBuckets(aggResp, aggParams, timeRange);
@@ -204,9 +209,9 @@ describe('Buckets wrapper', () => {
},
};
const timeRange = {
- gte: 150,
- lte: 350,
- name: 'date',
+ from: moment(150),
+ to: moment(350),
+ timeFields: ['date'],
};
const buckets = new TabifyBuckets(aggResp, aggParams, timeRange);
@@ -221,9 +226,9 @@ describe('Buckets wrapper', () => {
},
};
const timeRange = {
- gte: 100,
- lte: 400,
- name: 'date',
+ from: moment(100),
+ to: moment(400),
+ timeFields: ['date'],
};
const buckets = new TabifyBuckets(aggResp, aggParams, timeRange);
@@ -238,13 +243,47 @@ describe('Buckets wrapper', () => {
},
};
const timeRange = {
- gte: 150,
- lte: 350,
- name: 'date',
+ from: moment(150),
+ to: moment(350),
+ timeFields: ['date'],
};
const buckets = new TabifyBuckets(aggResp, aggParams, timeRange);
expect(buckets).toHaveLength(4);
});
+
+ test('does drop bucket when multiple time fields specified', () => {
+ const aggParams = {
+ drop_partials: true,
+ field: {
+ name: 'date',
+ },
+ };
+ const timeRange = {
+ from: moment(100),
+ to: moment(350),
+ timeFields: ['date', 'other_datefield'],
+ };
+ const buckets = new TabifyBuckets(aggResp, aggParams, timeRange);
+
+ expect(buckets.buckets.map((b: Bucket) => b.key)).toEqual([100, 200]);
+ });
+
+ test('does not drop bucket when no timeFields have been specified', () => {
+ const aggParams = {
+ drop_partials: true,
+ field: {
+ name: 'date',
+ },
+ };
+ const timeRange = {
+ from: moment(100),
+ to: moment(350),
+ timeFields: [],
+ };
+ const buckets = new TabifyBuckets(aggResp, aggParams, timeRange);
+
+ expect(buckets.buckets.map((b: Bucket) => b.key)).toEqual([0, 100, 200, 300]);
+ });
});
});
diff --git a/src/plugins/data/public/search/tabify/buckets.ts b/src/plugins/data/public/search/tabify/buckets.ts
index 971e820ac6ddf..cd52a09caeaad 100644
--- a/src/plugins/data/public/search/tabify/buckets.ts
+++ b/src/plugins/data/public/search/tabify/buckets.ts
@@ -20,7 +20,7 @@
import { get, isPlainObject, keys, findKey } from 'lodash';
import moment from 'moment';
import { IAggConfig } from '../aggs';
-import { AggResponseBucket, TabbedRangeFilterParams } from './types';
+import { AggResponseBucket, TabbedRangeFilterParams, TimeRangeInformation } from './types';
type AggParams = IAggConfig['params'] & {
drop_partials: boolean;
@@ -36,7 +36,7 @@ export class TabifyBuckets {
buckets: any;
_keys: any[] = [];
- constructor(aggResp: any, aggParams?: AggParams, timeRange?: TabbedRangeFilterParams) {
+ constructor(aggResp: any, aggParams?: AggParams, timeRange?: TimeRangeInformation) {
if (aggResp && aggResp.buckets) {
this.buckets = aggResp.buckets;
} else if (aggResp) {
@@ -107,12 +107,12 @@ export class TabifyBuckets {
// dropPartials should only be called if the aggParam setting is enabled,
// and the agg field is the same as the Time Range.
- private dropPartials(params: AggParams, timeRange?: TabbedRangeFilterParams) {
+ private dropPartials(params: AggParams, timeRange?: TimeRangeInformation) {
if (
!timeRange ||
this.buckets.length <= 1 ||
this.objectMode ||
- params.field.name !== timeRange.name
+ !timeRange.timeFields.includes(params.field.name)
) {
return;
}
@@ -120,10 +120,10 @@ export class TabifyBuckets {
const interval = this.buckets[1].key - this.buckets[0].key;
this.buckets = this.buckets.filter((bucket: AggResponseBucket) => {
- if (moment(bucket.key).isBefore(timeRange.gte)) {
+ if (moment(bucket.key).isBefore(timeRange.from)) {
return false;
}
- if (moment(bucket.key + interval).isAfter(timeRange.lte)) {
+ if (moment(bucket.key + interval).isAfter(timeRange.to)) {
return false;
}
return true;
diff --git a/src/plugins/data/public/search/tabify/tabify.ts b/src/plugins/data/public/search/tabify/tabify.ts
index e93e989034252..9cb55f94537c5 100644
--- a/src/plugins/data/public/search/tabify/tabify.ts
+++ b/src/plugins/data/public/search/tabify/tabify.ts
@@ -20,7 +20,7 @@
import { get } from 'lodash';
import { TabbedAggResponseWriter } from './response_writer';
import { TabifyBuckets } from './buckets';
-import { TabbedResponseWriterOptions, TabbedRangeFilterParams } from './types';
+import { TabbedResponseWriterOptions } from './types';
import { AggResponseBucket } from './types';
import { AggGroupNames, IAggConfigs } from '../aggs';
@@ -54,7 +54,7 @@ export function tabifyAggResponse(
switch (agg.type.type) {
case AggGroupNames.Buckets:
const aggBucket = get(bucket, agg.id);
- const tabifyBuckets = new TabifyBuckets(aggBucket, agg.params, timeRange);
+ const tabifyBuckets = new TabifyBuckets(aggBucket, agg.params, respOpts?.timeRange);
if (tabifyBuckets.length) {
tabifyBuckets.forEach((subBucket, tabifyBucketKey) => {
@@ -153,20 +153,6 @@ export function tabifyAggResponse(
doc_count: esResponse.hits.total,
};
- let timeRange: TabbedRangeFilterParams | undefined;
-
- // Extract the time range object if provided
- if (respOpts && respOpts.timeRange) {
- const [timeRangeKey] = Object.keys(respOpts.timeRange);
-
- if (timeRangeKey) {
- timeRange = {
- name: timeRangeKey,
- ...respOpts.timeRange[timeRangeKey],
- };
- }
- }
-
collectBucket(aggConfigs, write, topLevelBucket, '', 1);
return write.response();
diff --git a/src/plugins/data/public/search/tabify/types.ts b/src/plugins/data/public/search/tabify/types.ts
index 1e051880d3f19..72e91eb58c8a9 100644
--- a/src/plugins/data/public/search/tabify/types.ts
+++ b/src/plugins/data/public/search/tabify/types.ts
@@ -17,6 +17,7 @@
* under the License.
*/
+import { Moment } from 'moment';
import { RangeFilterParams } from '../../../common';
import { IAggConfig } from '../aggs';
@@ -25,11 +26,18 @@ export interface TabbedRangeFilterParams extends RangeFilterParams {
name: string;
}
+/** @internal */
+export interface TimeRangeInformation {
+ from?: Moment;
+ to?: Moment;
+ timeFields: string[];
+}
+
/** @internal **/
export interface TabbedResponseWriterOptions {
metricsAtAllLevels: boolean;
partialRows: boolean;
- timeRange?: { [key: string]: RangeFilterParams };
+ timeRange?: TimeRangeInformation;
}
/** @internal */
diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md
index 5d94b6516c2ba..df4ba23244b4d 100644
--- a/src/plugins/data/server/server.api.md
+++ b/src/plugins/data/server/server.api.md
@@ -408,6 +408,8 @@ export interface IIndexPattern {
// (undocumented)
fields: IFieldType[];
// (undocumented)
+ getTimeField?(): IFieldType | undefined;
+ // (undocumented)
id?: string;
// (undocumented)
timeFieldName?: string;
diff --git a/test/api_integration/apis/index.js b/test/api_integration/apis/index.js
index c5bfc847d0041..0c4028905657d 100644
--- a/test/api_integration/apis/index.js
+++ b/test/api_integration/apis/index.js
@@ -33,5 +33,6 @@ export default function({ loadTestFile }) {
loadTestFile(require.resolve('./status'));
loadTestFile(require.resolve('./stats'));
loadTestFile(require.resolve('./ui_metric'));
+ loadTestFile(require.resolve('./telemetry'));
});
}
diff --git a/test/api_integration/apis/telemetry/index.js b/test/api_integration/apis/telemetry/index.js
new file mode 100644
index 0000000000000..c79f5cb470890
--- /dev/null
+++ b/test/api_integration/apis/telemetry/index.js
@@ -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.
+ */
+
+export default function({ loadTestFile }) {
+ describe('Telemetry', () => {
+ loadTestFile(require.resolve('./telemetry_local'));
+ loadTestFile(require.resolve('./opt_in'));
+ loadTestFile(require.resolve('./telemetry_optin_notice_seen'));
+ });
+}
diff --git a/test/api_integration/apis/telemetry/opt_in.ts b/test/api_integration/apis/telemetry/opt_in.ts
new file mode 100644
index 0000000000000..e4654ee3985f3
--- /dev/null
+++ b/test/api_integration/apis/telemetry/opt_in.ts
@@ -0,0 +1,123 @@
+/*
+ * 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 expect from '@kbn/expect';
+
+import { TelemetrySavedObjectAttributes } from 'src/plugins/telemetry/server/telemetry_repository';
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function optInTest({ getService }: FtrProviderContext) {
+ const supertest = getService('supertest');
+ const kibanaServer = getService('kibanaServer');
+ describe('/api/telemetry/v2/optIn API', () => {
+ let defaultAttributes: TelemetrySavedObjectAttributes;
+ let kibanaVersion: any;
+ before(async () => {
+ const kibanaVersionAccessor = kibanaServer.version;
+ kibanaVersion = await kibanaVersionAccessor.get();
+ defaultAttributes =
+ (await getSavedObjectAttributes(supertest).catch(err => {
+ if (err.message === 'expected 200 "OK", got 404 "Not Found"') {
+ return null;
+ }
+ throw err;
+ })) || {};
+
+ expect(typeof kibanaVersion).to.eql('string');
+ expect(kibanaVersion.length).to.be.greaterThan(0);
+ });
+
+ afterEach(async () => {
+ await updateSavedObjectAttributes(supertest, defaultAttributes);
+ });
+
+ it('should support sending false with allowChangingOptInStatus true', async () => {
+ await updateSavedObjectAttributes(supertest, {
+ ...defaultAttributes,
+ allowChangingOptInStatus: true,
+ });
+ await postTelemetryV2Optin(supertest, false, 200);
+ const { enabled, lastVersionChecked } = await getSavedObjectAttributes(supertest);
+ expect(enabled).to.be(false);
+ expect(lastVersionChecked).to.be(kibanaVersion);
+ });
+
+ it('should support sending true with allowChangingOptInStatus true', async () => {
+ await updateSavedObjectAttributes(supertest, {
+ ...defaultAttributes,
+ allowChangingOptInStatus: true,
+ });
+ await postTelemetryV2Optin(supertest, true, 200);
+ const { enabled, lastVersionChecked } = await getSavedObjectAttributes(supertest);
+ expect(enabled).to.be(true);
+ expect(lastVersionChecked).to.be(kibanaVersion);
+ });
+
+ it('should not support sending false with allowChangingOptInStatus false', async () => {
+ await updateSavedObjectAttributes(supertest, {
+ ...defaultAttributes,
+ allowChangingOptInStatus: false,
+ });
+ await postTelemetryV2Optin(supertest, false, 400);
+ });
+
+ it('should not support sending true with allowChangingOptInStatus false', async () => {
+ await updateSavedObjectAttributes(supertest, {
+ ...defaultAttributes,
+ allowChangingOptInStatus: false,
+ });
+ await postTelemetryV2Optin(supertest, true, 400);
+ });
+
+ it('should not support sending null', async () => {
+ await postTelemetryV2Optin(supertest, null, 400);
+ });
+
+ it('should not support sending junk', async () => {
+ await postTelemetryV2Optin(supertest, 42, 400);
+ });
+ });
+}
+
+async function postTelemetryV2Optin(supertest: any, value: any, statusCode: number): Promise {
+ const { body } = await supertest
+ .post('/api/telemetry/v2/optIn')
+ .set('kbn-xsrf', 'xxx')
+ .send({ enabled: value })
+ .expect(statusCode);
+
+ return body;
+}
+
+async function updateSavedObjectAttributes(
+ supertest: any,
+ attributes: TelemetrySavedObjectAttributes
+): Promise {
+ return await supertest
+ .post('/api/saved_objects/telemetry/telemetry')
+ .query({ overwrite: true })
+ .set('kbn-xsrf', 'xxx')
+ .send({ attributes })
+ .expect(200);
+}
+
+async function getSavedObjectAttributes(supertest: any): Promise {
+ const { body } = await supertest.get('/api/saved_objects/telemetry/telemetry').expect(200);
+ return body.attributes;
+}
diff --git a/test/api_integration/apis/telemetry/telemetry_local.js b/test/api_integration/apis/telemetry/telemetry_local.js
new file mode 100644
index 0000000000000..84bfd8a755c11
--- /dev/null
+++ b/test/api_integration/apis/telemetry/telemetry_local.js
@@ -0,0 +1,133 @@
+/*
+ * 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 expect from '@kbn/expect';
+import _ from 'lodash';
+
+/*
+ * Create a single-level array with strings for all the paths to values in the
+ * source object, up to 3 deep. Going deeper than 3 causes a bit too much churn
+ * in the tests.
+ */
+function flatKeys(source) {
+ const recursivelyFlatKeys = (obj, path = [], depth = 0) => {
+ return depth < 3 && _.isObject(obj)
+ ? _.map(obj, (v, k) => recursivelyFlatKeys(v, [...path, k], depth + 1))
+ : path.join('.');
+ };
+
+ return _.uniq(_.flattenDeep(recursivelyFlatKeys(source))).sort((a, b) => a.localeCompare(b));
+}
+
+export default function({ getService }) {
+ const supertest = getService('supertest');
+
+ describe('/api/telemetry/v2/clusters/_stats', () => {
+ it('should pull local stats and validate data types', async () => {
+ const timeRange = {
+ min: '2018-07-23T22:07:00Z',
+ max: '2018-07-23T22:13:00Z',
+ };
+
+ const { body } = await supertest
+ .post('/api/telemetry/v2/clusters/_stats')
+ .set('kbn-xsrf', 'xxx')
+ .send({ timeRange, unencrypted: true })
+ .expect(200);
+
+ expect(body.length).to.be(1);
+ const stats = body[0];
+ expect(stats.collection).to.be('local');
+ expect(stats.stack_stats.kibana.count).to.be.a('number');
+ expect(stats.stack_stats.kibana.indices).to.be.a('number');
+ expect(stats.stack_stats.kibana.os.platforms[0].platform).to.be.a('string');
+ expect(stats.stack_stats.kibana.os.platforms[0].count).to.be(1);
+ expect(stats.stack_stats.kibana.os.platformReleases[0].platformRelease).to.be.a('string');
+ expect(stats.stack_stats.kibana.os.platformReleases[0].count).to.be(1);
+ expect(stats.stack_stats.kibana.plugins.telemetry.opt_in_status).to.be(false);
+ expect(stats.stack_stats.kibana.plugins.telemetry.usage_fetcher).to.be.a('string');
+ expect(stats.stack_stats.kibana.plugins.stack_management).to.be.an('object');
+ expect(stats.stack_stats.kibana.plugins.ui_metric).to.be.an('object');
+ expect(stats.stack_stats.kibana.plugins.application_usage).to.be.an('object');
+ expect(stats.stack_stats.kibana.plugins.kql.defaultQueryLanguage).to.be.a('string');
+ expect(stats.stack_stats.kibana.plugins['tsvb-validation']).to.be.an('object');
+ expect(stats.stack_stats.kibana.plugins.localization).to.be.an('object');
+ expect(stats.stack_stats.kibana.plugins.csp.strict).to.be(true);
+ expect(stats.stack_stats.kibana.plugins.csp.warnLegacyBrowsers).to.be(true);
+ expect(stats.stack_stats.kibana.plugins.csp.rulesChangedFromDefault).to.be(false);
+ });
+
+ it('should pull local stats and validate fields', async () => {
+ const timeRange = {
+ min: '2018-07-23T22:07:00Z',
+ max: '2018-07-23T22:13:00Z',
+ };
+
+ const { body } = await supertest
+ .post('/api/telemetry/v2/clusters/_stats')
+ .set('kbn-xsrf', 'xxx')
+ .send({ timeRange, unencrypted: true })
+ .expect(200);
+
+ const stats = body[0];
+
+ const actual = flatKeys(stats);
+ expect(actual).to.be.an('array');
+ const expected = [
+ 'cluster_name',
+ 'cluster_stats.cluster_uuid',
+ 'cluster_stats.indices.analysis',
+ 'cluster_stats.indices.completion',
+ 'cluster_stats.indices.count',
+ 'cluster_stats.indices.docs',
+ 'cluster_stats.indices.fielddata',
+ 'cluster_stats.indices.mappings',
+ 'cluster_stats.indices.query_cache',
+ 'cluster_stats.indices.segments',
+ 'cluster_stats.indices.shards',
+ 'cluster_stats.indices.store',
+ 'cluster_stats.nodes.count',
+ 'cluster_stats.nodes.discovery_types',
+ 'cluster_stats.nodes.fs',
+ 'cluster_stats.nodes.ingest',
+ 'cluster_stats.nodes.jvm',
+ 'cluster_stats.nodes.network_types',
+ 'cluster_stats.nodes.os',
+ 'cluster_stats.nodes.packaging_types',
+ 'cluster_stats.nodes.plugins',
+ 'cluster_stats.nodes.process',
+ 'cluster_stats.nodes.versions',
+ 'cluster_stats.status',
+ 'cluster_stats.timestamp',
+ 'cluster_uuid',
+ 'collection',
+ 'collectionSource',
+ 'stack_stats.kibana.count',
+ 'stack_stats.kibana.indices',
+ 'stack_stats.kibana.os',
+ 'stack_stats.kibana.plugins',
+ 'stack_stats.kibana.versions',
+ 'timestamp',
+ 'version',
+ ];
+
+ expect(expected.every(m => actual.includes(m))).to.be.ok();
+ });
+ });
+}
diff --git a/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts b/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts
new file mode 100644
index 0000000000000..4413c672fb46c
--- /dev/null
+++ b/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts
@@ -0,0 +1,59 @@
+/*
+ * 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 expect from '@kbn/expect';
+import { Client, DeleteDocumentParams, GetParams, GetResponse } from 'elasticsearch';
+import { TelemetrySavedObjectAttributes } from 'src/plugins/telemetry/server/telemetry_repository';
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function optInTest({ getService }: FtrProviderContext) {
+ const client: Client = getService('legacyEs');
+ const supertest = getService('supertest');
+
+ describe('/api/telemetry/v2/userHasSeenNotice API Telemetry User has seen OptIn Notice', () => {
+ it('should update telemetry setting field via PUT', async () => {
+ try {
+ await client.delete({
+ index: '.kibana',
+ id: 'telemetry:telemetry',
+ } as DeleteDocumentParams);
+ } catch (err) {
+ if (err.statusCode !== 404) {
+ throw err;
+ }
+ }
+
+ await supertest
+ .put('/api/telemetry/v2/userHasSeenNotice')
+ .set('kbn-xsrf', 'xxx')
+ .expect(200);
+
+ const {
+ _source: { telemetry },
+ }: GetResponse<{
+ telemetry: TelemetrySavedObjectAttributes;
+ }> = await client.get({
+ index: '.kibana',
+ id: 'telemetry:telemetry',
+ } as GetParams);
+
+ expect(telemetry.userHasSeenNotice).to.be(true);
+ });
+ });
+}
diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts
index 862e5127bb670..93debdcc37f0a 100644
--- a/test/functional/page_objects/common_page.ts
+++ b/test/functional/page_objects/common_page.ts
@@ -44,6 +44,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
ensureCurrentUrl: boolean;
shouldLoginIfPrompted: boolean;
useActualUrl: boolean;
+ insertTimestamp: boolean;
}
class CommonPage {
@@ -65,7 +66,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
* Logins to Kibana as default user and navigates to provided app
* @param appUrl Kibana URL
*/
- private async loginIfPrompted(appUrl: string) {
+ private async loginIfPrompted(appUrl: string, insertTimestamp: boolean) {
let currentUrl = await browser.getCurrentUrl();
log.debug(`currentUrl = ${currentUrl}\n appUrl = ${appUrl}`);
await testSubjects.find('kibanaChrome', 6 * defaultFindTimeout); // 60 sec waiting
@@ -87,7 +88,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
'[data-test-subj="kibanaChrome"] nav:not(.ng-hide)',
6 * defaultFindTimeout
);
- await browser.get(appUrl);
+ await browser.get(appUrl, insertTimestamp);
currentUrl = await browser.getCurrentUrl();
log.debug(`Finished login process currentUrl = ${currentUrl}`);
}
@@ -95,7 +96,13 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
}
private async navigate(navigateProps: NavigateProps) {
- const { appConfig, ensureCurrentUrl, shouldLoginIfPrompted, useActualUrl } = navigateProps;
+ const {
+ appConfig,
+ ensureCurrentUrl,
+ shouldLoginIfPrompted,
+ useActualUrl,
+ insertTimestamp,
+ } = navigateProps;
const appUrl = getUrl.noAuth(config.get('servers.kibana'), appConfig);
await retry.try(async () => {
@@ -111,7 +118,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
}
const currentUrl = shouldLoginIfPrompted
- ? await this.loginIfPrompted(appUrl)
+ ? await this.loginIfPrompted(appUrl, insertTimestamp)
: await browser.getCurrentUrl();
if (ensureCurrentUrl && !currentUrl.includes(appUrl)) {
@@ -134,6 +141,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
ensureCurrentUrl = true,
shouldLoginIfPrompted = true,
useActualUrl = false,
+ insertTimestamp = true,
} = {}
) {
const appConfig = {
@@ -146,6 +154,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
ensureCurrentUrl,
shouldLoginIfPrompted,
useActualUrl,
+ insertTimestamp,
});
}
@@ -165,6 +174,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
ensureCurrentUrl = true,
shouldLoginIfPrompted = true,
useActualUrl = true,
+ insertTimestamp = true,
} = {}
) {
const appConfig = {
@@ -178,6 +188,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
ensureCurrentUrl,
shouldLoginIfPrompted,
useActualUrl,
+ insertTimestamp,
});
}
@@ -208,7 +219,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
async navigateToApp(
appName: string,
- { basePath = '', shouldLoginIfPrompted = true, hash = '' } = {}
+ { basePath = '', shouldLoginIfPrompted = true, hash = '', insertTimestamp = true } = {}
) {
let appUrl: string;
if (config.has(['apps', appName])) {
@@ -239,7 +250,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
log.debug('returned from get, calling refresh');
await browser.refresh();
let currentUrl = shouldLoginIfPrompted
- ? await this.loginIfPrompted(appUrl)
+ ? await this.loginIfPrompted(appUrl, insertTimestamp)
: await browser.getCurrentUrl();
if (currentUrl.includes('app/kibana')) {
diff --git a/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts b/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts
new file mode 100644
index 0000000000000..5ea151dffdc8e
--- /dev/null
+++ b/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts
@@ -0,0 +1,93 @@
+/*
+ * 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 expect from '@kbn/expect';
+import { ExpectExpression, expectExpressionProvider } from './helpers';
+import { FtrProviderContext } from '../../../functional/ftr_provider_context';
+
+function getCell(esaggsResult: any, column: number, row: number): unknown | undefined {
+ const columnId = esaggsResult?.columns[column]?.id;
+ if (!columnId) {
+ return;
+ }
+ return esaggsResult?.rows[row]?.[columnId];
+}
+
+export default function({
+ getService,
+ updateBaselines,
+}: FtrProviderContext & { updateBaselines: boolean }) {
+ let expectExpression: ExpectExpression;
+ describe('esaggs pipeline expression tests', () => {
+ before(() => {
+ expectExpression = expectExpressionProvider({ getService, updateBaselines });
+ });
+
+ describe('correctly renders tagcloud', () => {
+ it('filters on index pattern primary date field by default', async () => {
+ const aggConfigs = [{ id: 1, enabled: true, type: 'count', schema: 'metric', params: {} }];
+ const timeRange = {
+ from: '2006-09-21T00:00:00Z',
+ to: '2015-09-22T00:00:00Z',
+ };
+ const expression = `
+ kibana_context timeRange='${JSON.stringify(timeRange)}'
+ | esaggs index='logstash-*' aggConfigs='${JSON.stringify(aggConfigs)}'
+ `;
+ const result = await expectExpression('esaggs_primary_timefield', expression).getResponse();
+ expect(getCell(result, 0, 0)).to.be(9375);
+ });
+
+ it('filters on the specified date field', async () => {
+ const aggConfigs = [{ id: 1, enabled: true, type: 'count', schema: 'metric', params: {} }];
+ const timeRange = {
+ from: '2006-09-21T00:00:00Z',
+ to: '2015-09-22T00:00:00Z',
+ };
+ const expression = `
+ kibana_context timeRange='${JSON.stringify(timeRange)}'
+ | esaggs index='logstash-*' timeFields='relatedContent.article:published_time' aggConfigs='${JSON.stringify(
+ aggConfigs
+ )}'
+ `;
+ const result = await expectExpression('esaggs_other_timefield', expression).getResponse();
+ expect(getCell(result, 0, 0)).to.be(11134);
+ });
+
+ it('filters on multiple specified date field', async () => {
+ const aggConfigs = [{ id: 1, enabled: true, type: 'count', schema: 'metric', params: {} }];
+ const timeRange = {
+ from: '2006-09-21T00:00:00Z',
+ to: '2015-09-22T00:00:00Z',
+ };
+ const expression = `
+ kibana_context timeRange='${JSON.stringify(timeRange)}'
+ | esaggs index='logstash-*' timeFields='relatedContent.article:published_time' timeFields='@timestamp' aggConfigs='${JSON.stringify(
+ aggConfigs
+ )}'
+ `;
+ const result = await expectExpression(
+ 'esaggs_multiple_timefields',
+ expression
+ ).getResponse();
+ expect(getCell(result, 0, 0)).to.be(7452);
+ });
+ });
+ });
+}
diff --git a/test/interpreter_functional/test_suites/run_pipeline/index.ts b/test/interpreter_functional/test_suites/run_pipeline/index.ts
index 031a0e3576ccc..9590f9f8c1794 100644
--- a/test/interpreter_functional/test_suites/run_pipeline/index.ts
+++ b/test/interpreter_functional/test_suites/run_pipeline/index.ts
@@ -46,5 +46,6 @@ export default function({ getService, getPageObjects, loadTestFile }: FtrProvide
loadTestFile(require.resolve('./basic'));
loadTestFile(require.resolve('./tag_cloud'));
loadTestFile(require.resolve('./metric'));
+ loadTestFile(require.resolve('./esaggs'));
});
}
diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js
index d1e8892fa2c98..a1186e04ee27a 100644
--- a/x-pack/legacy/plugins/maps/index.js
+++ b/x-pack/legacy/plugins/maps/index.js
@@ -54,7 +54,7 @@ export function maps(kibana) {
emsLandingPageUrl: mapConfig.emsLandingPageUrl,
kbnPkgVersion: serverConfig.get('pkg.version'),
regionmapLayers: _.get(mapConfig, 'regionmap.layers', []),
- tilemap: _.get(mapConfig, 'tilemap', []),
+ tilemap: _.get(mapConfig, 'tilemap', {}),
};
},
styleSheetPaths: `${__dirname}/public/index.scss`,
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
index 946f1c14bf593..cf691f73bdc2c 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
@@ -51,17 +51,19 @@ const getCurrentValueFromAggregations = (
const getParsedFilterQuery: (
filterQuery: string | undefined
-) => Record = filterQuery => {
+) => Record | Array> = filterQuery => {
if (!filterQuery) return {};
try {
return JSON.parse(filterQuery).bool;
} catch (e) {
- return {
- query_string: {
- query: filterQuery,
- analyze_wildcard: true,
+ return [
+ {
+ query_string: {
+ query: filterQuery,
+ analyze_wildcard: true,
+ },
},
- };
+ ];
}
};
@@ -159,8 +161,12 @@ export const getElasticsearchMetricQuery = (
return {
query: {
bool: {
- filter: [...rangeFilters, ...metricFieldFilters],
- ...parsedFilterQuery,
+ filter: [
+ ...rangeFilters,
+ ...metricFieldFilters,
+ ...(Array.isArray(parsedFilterQuery) ? parsedFilterQuery : []),
+ ],
+ ...(!Array.isArray(parsedFilterQuery) ? parsedFilterQuery : {}),
},
},
size: 0,
@@ -233,6 +239,7 @@ const getMetric: (
body: searchBody,
index,
});
+
return { '*': getCurrentValueFromAggregations(result.aggregations, aggType) };
} catch (e) {
return { '*': undefined }; // Trigger an Error state
diff --git a/x-pack/plugins/ingest_manager/common/constants/agent_config.ts b/x-pack/plugins/ingest_manager/common/constants/agent_config.ts
index c5067480fb953..9bc1293799d3c 100644
--- a/x-pack/plugins/ingest_manager/common/constants/agent_config.ts
+++ b/x-pack/plugins/ingest_manager/common/constants/agent_config.ts
@@ -14,6 +14,7 @@ export const DEFAULT_AGENT_CONFIG = {
status: AgentConfigStatus.Active,
datasources: [],
is_default: true,
+ monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>,
};
export const DEFAULT_AGENT_CONFIGS_PACKAGES = [DefaultPackages.system];
diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts
index a7d4e36d16f2a..bff799798ff6e 100644
--- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts
+++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts
@@ -3,11 +3,12 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { Datasource, NewDatasource, DatasourceInput } from '../types';
+import { Datasource, DatasourceInput } from '../types';
import { storedDatasourceToAgentDatasource } from './datasource_to_agent_datasource';
describe('Ingest Manager - storedDatasourceToAgentDatasource', () => {
- const mockNewDatasource: NewDatasource = {
+ const mockDatasource: Datasource = {
+ id: 'some-uuid',
name: 'mock-datasource',
description: '',
config_id: '',
@@ -15,11 +16,6 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => {
output_id: '',
namespace: 'default',
inputs: [],
- };
-
- const mockDatasource: Datasource = {
- ...mockNewDatasource,
- id: 'some-uuid',
revision: 1,
};
@@ -107,17 +103,6 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => {
});
});
- it('uses name for id when id is not provided in case of new datasource', () => {
- expect(storedDatasourceToAgentDatasource(mockNewDatasource)).toEqual({
- id: 'mock-datasource',
- name: 'mock-datasource',
- namespace: 'default',
- enabled: true,
- use_output: 'default',
- inputs: [],
- });
- });
-
it('returns agent datasource config with flattened input and package stream', () => {
expect(storedDatasourceToAgentDatasource({ ...mockDatasource, inputs: [mockInput] })).toEqual({
id: 'some-uuid',
diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts
index 5deb33ccf10f1..620b663451ea3 100644
--- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts
+++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts
@@ -3,16 +3,16 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { Datasource, NewDatasource, FullAgentConfigDatasource } from '../types';
+import { Datasource, FullAgentConfigDatasource } from '../types';
import { DEFAULT_OUTPUT } from '../constants';
export const storedDatasourceToAgentDatasource = (
- datasource: Datasource | NewDatasource
+ datasource: Datasource
): FullAgentConfigDatasource => {
- const { name, namespace, enabled, package: pkg, inputs } = datasource;
+ const { id, name, namespace, enabled, package: pkg, inputs } = datasource;
const fullDatasource: FullAgentConfigDatasource = {
- id: 'id' in datasource ? datasource.id : name,
+ id: id || name,
name,
namespace,
enabled,
diff --git a/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts b/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts
index cb290e61b17e5..a977a1a66e059 100644
--- a/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts
+++ b/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts
@@ -11,6 +11,7 @@ describe('Ingest Manager - packageToConfig', () => {
name: 'mock-package',
title: 'Mock package',
version: '0.0.0',
+ latestVersion: '0.0.0',
description: 'description',
type: 'mock',
categories: [],
diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts
index 2372caee512af..96121251b133e 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts
@@ -3,8 +3,6 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-
-import { SavedObjectAttributes } from 'src/core/public';
import {
Datasource,
DatasourcePackage,
@@ -23,9 +21,10 @@ export interface NewAgentConfig {
namespace?: string;
description?: string;
is_default?: boolean;
+ monitoring_enabled?: Array<'logs' | 'metrics'>;
}
-export interface AgentConfig extends NewAgentConfig, SavedObjectAttributes {
+export interface AgentConfig extends NewAgentConfig {
id: string;
status: AgentConfigStatus;
datasources: string[] | Datasource[];
@@ -60,4 +59,12 @@ export interface FullAgentConfig {
};
datasources: FullAgentConfigDatasource[];
revision?: number;
+ settings?: {
+ monitoring: {
+ use_output?: string;
+ enabled: boolean;
+ metrics: boolean;
+ logs: boolean;
+ };
+ };
}
diff --git a/x-pack/plugins/ingest_manager/common/types/models/datasource.ts b/x-pack/plugins/ingest_manager/common/types/models/datasource.ts
index 885e0a9316d79..ca61a93d9be93 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/datasource.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/datasource.ts
@@ -17,22 +17,29 @@ export interface DatasourceConfigRecordEntry {
export type DatasourceConfigRecord = Record;
-export interface DatasourceInputStream {
+export interface NewDatasourceInputStream {
id: string;
enabled: boolean;
dataset: string;
processors?: string[];
config?: DatasourceConfigRecord;
vars?: DatasourceConfigRecord;
+}
+
+export interface DatasourceInputStream extends NewDatasourceInputStream {
agent_stream?: any;
}
-export interface DatasourceInput {
+export interface NewDatasourceInput {
type: string;
enabled: boolean;
processors?: string[];
config?: DatasourceConfigRecord;
vars?: DatasourceConfigRecord;
+ streams: NewDatasourceInputStream[];
+}
+
+export interface DatasourceInput extends Omit {
streams: DatasourceInputStream[];
}
@@ -44,10 +51,11 @@ export interface NewDatasource {
enabled: boolean;
package?: DatasourcePackage;
output_id: string;
- inputs: DatasourceInput[];
+ inputs: NewDatasourceInput[];
}
-export type Datasource = NewDatasource & {
+export interface Datasource extends Omit {
id: string;
+ inputs: DatasourceInput[];
revision: number;
-};
+}
diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts
index 05e160cdfb81a..f8779a879a049 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts
@@ -204,6 +204,8 @@ export interface RegistryVarsEntry {
// internal until we need them
interface PackageAdditions {
title: string;
+ latestVersion: string;
+ installedVersion?: string;
assets: AssetsGroupedByServiceByType;
}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx
new file mode 100644
index 0000000000000..1e7a14e350229
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx
@@ -0,0 +1,101 @@
+/*
+ * 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 {
+ EuiButtonEmpty,
+ EuiFlyout,
+ EuiFlyoutBody,
+ EuiFlyoutHeader,
+ EuiFlyoutFooter,
+ EuiLink,
+ EuiText,
+ EuiTitle,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+interface Props {
+ onClose: () => void;
+}
+
+export const AlphaFlyout: React.FunctionComponent = ({ onClose }) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ),
+ forumLink: (
+
+
+
+ ),
+ }}
+ />
+
+
+
+
+
+
+ ),
+ }}
+ />
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx
index 0f3ddee29fa44..5a06a9a879441 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx
@@ -3,35 +3,45 @@
* 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 React, { useState } from 'react';
import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiText } from '@elastic/eui';
+import { EuiText, EuiLink } from '@elastic/eui';
+import { AlphaFlyout } from './alpha_flyout';
const Message = styled(EuiText).attrs(props => ({
color: 'subdued',
textAlign: 'center',
+ size: 's',
}))`
padding: ${props => props.theme.eui.paddingSizes.m};
`;
-export const AlphaMessaging: React.FC<{}> = () => (
-
-
-
-
+export const AlphaMessaging: React.FC<{}> = () => {
+ const [isAlphaFlyoutOpen, setIsAlphaFlyoutOpen] = useState(false);
+
+ return (
+ <>
+
+
+
+
+
+ {' – '}
-
- {' – '}
-
-
-
-
-);
+ />{' '}
+ setIsAlphaFlyoutOpen(true)}>
+ View more details.
+
+
+
+ {isAlphaFlyoutOpen && setIsAlphaFlyoutOpen(false)} />}
+ >
+ );
+};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts
index 0d19ecd0cb735..e2fc190e158f9 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts
@@ -5,12 +5,18 @@
*/
import { sendRequest, useRequest } from './use_request';
import { datasourceRouteService } from '../../services';
-import { CreateDatasourceRequest, CreateDatasourceResponse } from '../../types';
+import {
+ CreateDatasourceRequest,
+ CreateDatasourceResponse,
+ UpdateDatasourceRequest,
+ UpdateDatasourceResponse,
+} from '../../types';
import {
DeleteDatasourcesRequest,
DeleteDatasourcesResponse,
GetDatasourcesRequest,
GetDatasourcesResponse,
+ GetOneDatasourceResponse,
} from '../../../../../common/types/rest_spec';
export const sendCreateDatasource = (body: CreateDatasourceRequest['body']) => {
@@ -21,6 +27,17 @@ export const sendCreateDatasource = (body: CreateDatasourceRequest['body']) => {
});
};
+export const sendUpdateDatasource = (
+ datasourceId: string,
+ body: UpdateDatasourceRequest['body']
+) => {
+ return sendRequest({
+ path: datasourceRouteService.getUpdatePath(datasourceId),
+ method: 'put',
+ body: JSON.stringify(body),
+ });
+};
+
export const sendDeleteDatasource = (body: DeleteDatasourcesRequest['body']) => {
return sendRequest({
path: datasourceRouteService.getDeletePath(),
@@ -36,3 +53,10 @@ export function useGetDatasources(query: GetDatasourcesRequest['query']) {
query,
});
}
+
+export const sendGetOneDatasource = (datasourceId: string) => {
+ return sendRequest({
+ path: datasourceRouteService.getInfoPath(datasourceId),
+ method: 'get',
+ });
+};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx
index 0d53ca34a1fef..92c44d86e47c6 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx
@@ -18,6 +18,7 @@ import {
EuiText,
EuiComboBox,
EuiIconTip,
+ EuiCheckboxGroup,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
@@ -30,7 +31,7 @@ interface ValidationResults {
const StyledEuiAccordion = styled(EuiAccordion)`
.ingest-active-button {
- color: ${props => props.theme.eui.euiColorPrimary}};
+ color: ${props => props.theme.eui.euiColorPrimary};
}
`;
@@ -244,6 +245,68 @@ export const AgentConfigForm: React.FunctionComponent = ({
)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ acc[key] = true;
+ return acc;
+ },
+ { logs: false, metrics: false }
+ )}
+ onChange={id => {
+ if (id !== 'logs' && id !== 'metrics') {
+ return;
+ }
+
+ const hasLogs =
+ agentConfig.monitoring_enabled && agentConfig.monitoring_enabled.indexOf(id) >= 0;
+
+ const previousValues = agentConfig.monitoring_enabled || [];
+ updateAgentConfig({
+ monitoring_enabled: hasLogs
+ ? previousValues.filter(type => type !== id)
+ : [...previousValues, id],
+ });
+ }}
+ />
+
+
);
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx
index 39d882f7fdf65..f1e3fea6a0742 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx
@@ -39,17 +39,29 @@ export const CreateDatasourcePageLayout: React.FunctionComponent<{
-
+ {from === 'edit' ? (
+
+ ) : (
+
+ )}
- {from === 'config' ? (
+ {from === 'edit' ? (
+
+ ) : from === 'config' ? (
- {agentConfig && from === 'config' ? (
+ {agentConfig && (from === 'config' || from === 'edit') ? (
{
const updatePackageInfo = (updatedPackageInfo: PackageInfo | undefined) => {
if (updatedPackageInfo) {
setPackageInfo(updatedPackageInfo);
+ setFormState('VALID');
} else {
setFormState('INVALID');
setPackageInfo(undefined);
@@ -152,9 +153,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => {
const cancelUrl = from === 'config' ? CONFIG_URL : PACKAGE_URL;
// Save datasource
- const [formState, setFormState] = useState<
- 'VALID' | 'INVALID' | 'CONFIRM' | 'LOADING' | 'SUBMITTED'
- >('INVALID');
+ const [formState, setFormState] = useState('INVALID');
const saveDatasource = async () => {
setFormState('LOADING');
const result = await sendCreateDatasource(datasource);
@@ -174,6 +173,23 @@ export const CreateDatasourcePage: React.FunctionComponent = () => {
const { error } = await saveDatasource();
if (!error) {
history.push(`${AGENT_CONFIG_DETAILS_PATH}${agentConfig ? agentConfig.id : configId}`);
+ notifications.toasts.addSuccess({
+ title: i18n.translate('xpack.ingestManager.createDatasource.addedNotificationTitle', {
+ defaultMessage: `Successfully added '{datasourceName}'`,
+ values: {
+ datasourceName: datasource.name,
+ },
+ }),
+ text:
+ agentCount && agentConfig
+ ? i18n.translate('xpack.ingestManager.createDatasource.addedNotificationMessage', {
+ defaultMessage: `Fleet will deploy updates to all agents that use the '{agentConfigName}' configuration`,
+ values: {
+ agentConfigName: agentConfig.name,
+ },
+ })
+ : undefined,
+ });
} else {
notifications.toasts.addError(error, {
title: 'Error',
@@ -229,6 +245,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => {
packageInfo={packageInfo}
datasource={datasource}
updateDatasource={updateDatasource}
+ validationResults={validationResults!}
/>
) : null,
},
@@ -240,7 +257,6 @@ export const CreateDatasourcePage: React.FunctionComponent = () => {
children:
agentConfig && packageInfo ? (
) => void;
validationResults: DatasourceValidationResults;
submitAttempted: boolean;
-}> = ({
- agentConfig,
- packageInfo,
- datasource,
- updateDatasource,
- validationResults,
- submitAttempted,
-}) => {
- // Form show/hide states
-
+}> = ({ packageInfo, datasource, updateDatasource, validationResults, submitAttempted }) => {
const hasErrors = validationResults ? validationHasErrors(validationResults) : false;
- // Update datasource's package and config info
- useEffect(() => {
- const dsPackage = datasource.package;
- const currentPkgKey = dsPackage ? `${dsPackage.name}-${dsPackage.version}` : '';
- const pkgKey = `${packageInfo.name}-${packageInfo.version}`;
-
- // If package has changed, create shell datasource with input&stream values based on package info
- if (currentPkgKey !== pkgKey) {
- // Existing datasources on the agent config using the package name, retrieve highest number appended to datasource name
- const dsPackageNamePattern = new RegExp(`${packageInfo.name}-(\\d+)`);
- const dsWithMatchingNames = (agentConfig.datasources as Datasource[])
- .filter(ds => Boolean(ds.name.match(dsPackageNamePattern)))
- .map(ds => parseInt(ds.name.match(dsPackageNamePattern)![1], 10))
- .sort();
-
- updateDatasource({
- name: `${packageInfo.name}-${
- dsWithMatchingNames.length ? dsWithMatchingNames[dsWithMatchingNames.length - 1] + 1 : 1
- }`,
- package: {
- name: packageInfo.name,
- title: packageInfo.title,
- version: packageInfo.version,
- },
- inputs: packageToConfigDatasourceInputs(packageInfo),
- });
- }
-
- // If agent config has changed, update datasource's config ID and namespace
- if (datasource.config_id !== agentConfig.id) {
- updateDatasource({
- config_id: agentConfig.id,
- namespace: agentConfig.namespace,
- });
- }
- }, [datasource.package, datasource.config_id, agentConfig, packageInfo, updateDatasource]);
-
- // Step B, configure inputs (and their streams)
+ // Configure inputs (and their streams)
// Assume packages only export one datasource for now
const renderConfigureInputs = () =>
packageInfo.datasources &&
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx
index 792389381eaf0..c4d602c2c2081 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx
@@ -17,13 +17,16 @@ import {
} from '@elastic/eui';
import { AgentConfig, PackageInfo, Datasource, NewDatasource } from '../../../types';
import { packageToConfigDatasourceInputs } from '../../../services';
+import { Loading } from '../../../components';
+import { DatasourceValidationResults } from './services';
export const StepDefineDatasource: React.FunctionComponent<{
agentConfig: AgentConfig;
packageInfo: PackageInfo;
datasource: NewDatasource;
updateDatasource: (fields: Partial) => void;
-}> = ({ agentConfig, packageInfo, datasource, updateDatasource }) => {
+ validationResults: DatasourceValidationResults;
+}> = ({ agentConfig, packageInfo, datasource, updateDatasource, validationResults }) => {
// Form show/hide states
const [isShowingAdvancedDefine, setIsShowingAdvancedDefine] = useState(false);
@@ -64,11 +67,13 @@ export const StepDefineDatasource: React.FunctionComponent<{
}
}, [datasource.package, datasource.config_id, agentConfig, packageInfo, updateDatasource]);
- return (
+ return validationResults ? (
<>
}
+ isInvalid={!!validationResults.description}
+ error={validationResults.description}
>
) : null}
>
+ ) : (
+
);
};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts
index 85cc758fc4c46..10b30a5696d83 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts
@@ -4,4 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export type CreateDatasourceFrom = 'package' | 'config';
+export type CreateDatasourceFrom = 'package' | 'config' | 'edit';
+export type DatasourceFormState = 'VALID' | 'INVALID' | 'CONFIRM' | 'LOADING' | 'SUBMITTED';
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx
index 1eee9f6b0c346..a0418c5f256c4 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx
@@ -19,7 +19,7 @@ import {
import { AgentConfig, Datasource } from '../../../../../types';
import { TableRowActions } from '../../../components/table_row_actions';
import { DangerEuiContextMenuItem } from '../../../components/danger_eui_context_menu_item';
-import { useCapabilities } from '../../../../../hooks';
+import { useCapabilities, useLink } from '../../../../../hooks';
import { useAgentConfigLink } from '../../hooks/use_details_uri';
import { DatasourceDeleteProvider } from '../../../components/datasource_delete_provider';
import { useConfigRefresh } from '../../hooks/use_config';
@@ -56,6 +56,7 @@ export const DatasourcesTable: React.FunctionComponent = ({
}) => {
const hasWriteCapabilities = useCapabilities().write;
const addDatasourceLink = useAgentConfigLink('add-datasource', { configId: config.id });
+ const editDatasourceLink = useLink(`/configs/${config.id}/edit-datasource`);
const refreshConfig = useConfigRefresh();
// With the datasources provided on input, generate the list of datasources
@@ -201,22 +202,21 @@ export const DatasourcesTable: React.FunctionComponent = ({
{}}
+ // key="datasourceView"
+ // >
+ //
+ // ,
{}}
- key="datasourceView"
- >
-
- ,
- // FIXME: implement Edit datasource action
- {}}
+ href={`${editDatasourceLink}/${datasource.id}`}
key="datasourceEdit"
>
= ({
/>
,
// FIXME: implement Copy datasource action
- {}} key="datasourceCopy">
-
- ,
+ // {}} key="datasourceCopy">
+ //
+ // ,
{deleteDatasourcePrompt => {
return (
@@ -256,7 +256,7 @@ export const DatasourcesTable: React.FunctionComponent = ({
],
},
],
- [config, hasWriteCapabilities, refreshConfig]
+ [config, editDatasourceLink, hasWriteCapabilities, refreshConfig]
);
return (
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx
new file mode 100644
index 0000000000000..d4c39f21a1ea6
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx
@@ -0,0 +1,323 @@
+/*
+ * 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, { useState, useEffect } from 'react';
+import { useRouteMatch, useHistory } from 'react-router-dom';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiButtonEmpty,
+ EuiButton,
+ EuiSteps,
+ EuiBottomBar,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSpacer,
+} from '@elastic/eui';
+import { AGENT_CONFIG_DETAILS_PATH } from '../../../constants';
+import { AgentConfig, PackageInfo, NewDatasource } from '../../../types';
+import {
+ useLink,
+ useCore,
+ useConfig,
+ sendUpdateDatasource,
+ sendGetAgentStatus,
+ sendGetOneAgentConfig,
+ sendGetOneDatasource,
+ sendGetPackageInfoByKey,
+} from '../../../hooks';
+import { Loading, Error } from '../../../components';
+import {
+ CreateDatasourcePageLayout,
+ ConfirmCreateDatasourceModal,
+} from '../create_datasource_page/components';
+import {
+ DatasourceValidationResults,
+ validateDatasource,
+ validationHasErrors,
+} from '../create_datasource_page/services';
+import { DatasourceFormState, CreateDatasourceFrom } from '../create_datasource_page/types';
+import { StepConfigureDatasource } from '../create_datasource_page/step_configure_datasource';
+import { StepDefineDatasource } from '../create_datasource_page/step_define_datasource';
+
+export const EditDatasourcePage: React.FunctionComponent = () => {
+ const { notifications } = useCore();
+ const {
+ fleet: { enabled: isFleetEnabled },
+ } = useConfig();
+ const {
+ params: { configId, datasourceId },
+ } = useRouteMatch();
+ const history = useHistory();
+
+ // Agent config, package info, and datasource states
+ const [isLoadingData, setIsLoadingData] = useState(true);
+ const [loadingError, setLoadingError] = useState();
+ const [agentConfig, setAgentConfig] = useState();
+ const [packageInfo, setPackageInfo] = useState();
+ const [datasource, setDatasource] = useState({
+ name: '',
+ description: '',
+ config_id: '',
+ enabled: true,
+ output_id: '',
+ inputs: [],
+ });
+
+ // Retrieve agent config, package, and datasource info
+ useEffect(() => {
+ const getData = async () => {
+ setIsLoadingData(true);
+ setLoadingError(undefined);
+ try {
+ const [{ data: agentConfigData }, { data: datasourceData }] = await Promise.all([
+ sendGetOneAgentConfig(configId),
+ sendGetOneDatasource(datasourceId),
+ ]);
+ if (agentConfigData?.item) {
+ setAgentConfig(agentConfigData.item);
+ }
+ if (datasourceData?.item) {
+ const { id, revision, inputs, ...restOfDatasource } = datasourceData.item;
+ // Remove `agent_stream` from all stream info, we assign this after saving
+ const newDatasource = {
+ ...restOfDatasource,
+ inputs: inputs.map(input => {
+ const { streams, ...restOfInput } = input;
+ return {
+ ...restOfInput,
+ streams: streams.map(stream => {
+ const { agent_stream, ...restOfStream } = stream;
+ return restOfStream;
+ }),
+ };
+ }),
+ };
+ setDatasource(newDatasource);
+ if (datasourceData.item.package) {
+ const { data: packageData } = await sendGetPackageInfoByKey(
+ `${datasourceData.item.package.name}-${datasourceData.item.package.version}`
+ );
+ if (packageData?.response) {
+ setPackageInfo(packageData.response);
+ setValidationResults(validateDatasource(newDatasource, packageData.response));
+ setFormState('VALID');
+ }
+ }
+ }
+ } catch (e) {
+ setLoadingError(e);
+ }
+ setIsLoadingData(false);
+ };
+ getData();
+ }, [configId, datasourceId]);
+
+ // Retrieve agent count
+ const [agentCount, setAgentCount] = useState(0);
+ useEffect(() => {
+ const getAgentCount = async () => {
+ const { data } = await sendGetAgentStatus({ configId });
+ if (data?.results.total) {
+ setAgentCount(data.results.total);
+ }
+ };
+
+ if (isFleetEnabled) {
+ getAgentCount();
+ }
+ }, [configId, isFleetEnabled]);
+
+ // Datasource validation state
+ const [validationResults, setValidationResults] = useState();
+ const hasErrors = validationResults ? validationHasErrors(validationResults) : false;
+
+ // Update datasource method
+ const updateDatasource = (updatedFields: Partial) => {
+ const newDatasource = {
+ ...datasource,
+ ...updatedFields,
+ };
+ setDatasource(newDatasource);
+
+ // eslint-disable-next-line no-console
+ console.debug('Datasource updated', newDatasource);
+ const newValidationResults = updateDatasourceValidation(newDatasource);
+ const hasValidationErrors = newValidationResults
+ ? validationHasErrors(newValidationResults)
+ : false;
+ if (!hasValidationErrors) {
+ setFormState('VALID');
+ }
+ };
+
+ const updateDatasourceValidation = (newDatasource?: NewDatasource) => {
+ if (packageInfo) {
+ const newValidationResult = validateDatasource(newDatasource || datasource, packageInfo);
+ setValidationResults(newValidationResult);
+ // eslint-disable-next-line no-console
+ console.debug('Datasource validation results', newValidationResult);
+
+ return newValidationResult;
+ }
+ };
+
+ // Cancel url
+ const CONFIG_URL = useLink(`${AGENT_CONFIG_DETAILS_PATH}${configId}`);
+ const cancelUrl = CONFIG_URL;
+
+ // Save datasource
+ const [formState, setFormState] = useState('INVALID');
+ const saveDatasource = async () => {
+ setFormState('LOADING');
+ const result = await sendUpdateDatasource(datasourceId, datasource);
+ setFormState('SUBMITTED');
+ return result;
+ };
+
+ const onSubmit = async () => {
+ if (formState === 'VALID' && hasErrors) {
+ setFormState('INVALID');
+ return;
+ }
+ if (agentCount !== 0 && formState !== 'CONFIRM') {
+ setFormState('CONFIRM');
+ return;
+ }
+ const { error } = await saveDatasource();
+ if (!error) {
+ history.push(`${AGENT_CONFIG_DETAILS_PATH}${configId}`);
+ notifications.toasts.addSuccess({
+ title: i18n.translate('xpack.ingestManager.editDatasource.updatedNotificationTitle', {
+ defaultMessage: `Successfully updated '{datasourceName}'`,
+ values: {
+ datasourceName: datasource.name,
+ },
+ }),
+ text:
+ agentCount && agentConfig
+ ? i18n.translate('xpack.ingestManager.editDatasource.updatedNotificationMessage', {
+ defaultMessage: `Fleet will deploy updates to all agents that use the '{agentConfigName}' configuration`,
+ values: {
+ agentConfigName: agentConfig.name,
+ },
+ })
+ : undefined,
+ });
+ } else {
+ notifications.toasts.addError(error, {
+ title: 'Error',
+ });
+ setFormState('VALID');
+ }
+ };
+
+ const layoutProps = {
+ from: 'edit' as CreateDatasourceFrom,
+ cancelUrl,
+ agentConfig,
+ packageInfo,
+ };
+
+ return (
+
+ {isLoadingData ? (
+
+ ) : loadingError || !agentConfig || !packageInfo ? (
+
+ }
+ error={
+ loadingError ||
+ i18n.translate('xpack.ingestManager.editDatasource.errorLoadingDataMessage', {
+ defaultMessage: 'There was an error loading this data source information',
+ })
+ }
+ />
+ ) : (
+ <>
+ {formState === 'CONFIRM' && (
+ setFormState('VALID')}
+ />
+ )}
+
+ ),
+ },
+ {
+ title: i18n.translate(
+ 'xpack.ingestManager.editDatasource.stepConfgiureDatasourceTitle',
+ {
+ defaultMessage: 'Select the data you want to collect',
+ }
+ ),
+ children: (
+
+ ),
+ },
+ ]}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+ );
+};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx
index 71ada155373bf..ef88aa5d17f1e 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx
@@ -8,10 +8,14 @@ import { HashRouter as Router, Switch, Route } from 'react-router-dom';
import { AgentConfigListPage } from './list_page';
import { AgentConfigDetailsPage } from './details_page';
import { CreateDatasourcePage } from './create_datasource_page';
+import { EditDatasourcePage } from './edit_datasource_page';
export const AgentConfigApp: React.FunctionComponent = () => (
+
+
+
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx
index 1fe116ef36090..9f582e7e2fbe6 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx
@@ -34,6 +34,7 @@ export const CreateAgentConfigFlyout: React.FunctionComponent = ({ onClos
description: '',
namespace: '',
is_default: undefined,
+ monitoring_enabled: ['logs', 'metrics'],
});
const [isLoading, setIsLoading] = useState(false);
const [withSysMonitoring, setWithSysMonitoring] = useState(true);
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx
index 1ea162252c741..3dcc19bc4a5ae 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx
@@ -36,13 +36,11 @@ import {
useConfig,
useUrlParams,
} from '../../../hooks';
-import { AgentConfigDeleteProvider } from '../components';
import { CreateAgentConfigFlyout } from './components';
import { SearchBar } from '../../../components/search_bar';
import { LinkedAgentCount } from '../components';
import { useAgentConfigLink } from '../details_page/hooks/use_details_uri';
import { TableRowActions } from '../components/table_row_actions';
-import { DangerEuiContextMenuItem } from '../components/danger_eui_context_menu_item';
const NO_WRAP_TRUNCATE_STYLE: CSSProperties = Object.freeze({
overflow: 'hidden',
@@ -108,30 +106,12 @@ const ConfigRowActions = memo<{ config: AgentConfig; onDelete: () => void }>(
defaultMessage="Create data source"
/>
,
-
-
-
- ,
-
-
- {deleteAgentConfigsPrompt => {
- return (
- deleteAgentConfigsPrompt([config.id], onDelete)}
- >
-
-
- );
- }}
- ,
+ //
+ //
+ // ,
]}
/>
);
@@ -156,7 +136,6 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => {
: urlParams.kuery ?? ''
);
const { pagination, pageSizeOptions, setPagination } = usePagination();
- const [selectedAgentConfigs, setSelectedAgentConfigs] = useState([]);
const history = useHistory();
const isCreateAgentConfigFlyoutOpen = 'create' in urlParams;
const setIsCreateAgentConfigFlyoutOpen = useCallback(
@@ -321,34 +300,6 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => {
/>
) : null}
- {selectedAgentConfigs.length ? (
-
-
- {deleteAgentConfigsPrompt => (
- {
- deleteAgentConfigsPrompt(
- selectedAgentConfigs.map(agentConfig => agentConfig.id),
- () => {
- sendRequest();
- setSelectedAgentConfigs([]);
- }
- );
- }}
- >
-
-
- )}
-
-
- ) : null}
= () => {
items={agentConfigData ? agentConfigData.items : []}
itemId="id"
columns={columns}
- isSelectable={true}
- selection={{
- selectable: (agentConfig: AgentConfig) => !agentConfig.is_default,
- onSelectionChange: (newSelectedAgentConfigs: AgentConfig[]) => {
- setSelectedAgentConfigs(newSelectedAgentConfigs);
- },
- }}
+ isSelectable={false}
pagination={{
pageIndex: pagination.currentPage - 1,
pageSize: pagination.pageSize,
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx
new file mode 100644
index 0000000000000..64223efefaab8
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx
@@ -0,0 +1,15 @@
+/*
+ * 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 { EuiIcon } from '@elastic/eui';
+import React from 'react';
+import styled from 'styled-components';
+
+export const StyledAlert = styled(EuiIcon)`
+ color: ${props => props.theme.eui.euiColorWarning};
+ padding: 0 5px;
+`;
+
+export const UpdateIcon = () => ;
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx
index 8ad081cbbabe4..ab7e87b3ad06c 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx
@@ -30,9 +30,15 @@ export function PackageCard({
showInstalledBadge,
status,
icons,
+ ...restProps
}: PackageCardProps) {
const { toDetailView } = useLinks();
- const url = toDetailView({ name, version });
+ let urlVersion = version;
+ // if this is an installed package, link to the version installed
+ if ('savedObject' in restProps) {
+ urlVersion = restProps.savedObject.attributes.version || version;
+ }
+ const url = toDetailView({ name, version: urlVersion });
return (
;
+type InstallPackageProps = Pick & {
+ fromUpdate?: boolean;
+};
+type SetPackageInstallStatusProps = Pick & PackageInstallItem;
function usePackageInstall({ notifications }: { notifications: NotificationsStart }) {
+ const { toDetailView } = useLinks();
const [packages, setPackage] = useState({});
const setPackageInstallStatus = useCallback(
- ({ name, status }: { name: PackageInfo['name']; status: InstallStatus }) => {
+ ({ name, status, version }: SetPackageInstallStatusProps) => {
+ const packageProps: PackageInstallItem = {
+ status,
+ version,
+ };
setPackage((prev: PackagesInstall) => ({
...prev,
- [name]: { status },
+ [name]: packageProps,
}));
},
[]
);
+ const getPackageInstallStatus = useCallback(
+ (pkg: string): PackageInstallItem => {
+ return packages[pkg];
+ },
+ [packages]
+ );
+
const installPackage = useCallback(
- async ({ name, version, title }: InstallPackageProps) => {
- setPackageInstallStatus({ name, status: InstallStatus.installing });
+ async ({ name, version, title, fromUpdate = false }: InstallPackageProps) => {
+ const currStatus = getPackageInstallStatus(name);
+ const newStatus = { ...currStatus, name, status: InstallStatus.installing };
+ setPackageInstallStatus(newStatus);
const pkgkey = `${name}-${version}`;
const res = await sendInstallPackage(pkgkey);
if (res.error) {
- setPackageInstallStatus({ name, status: InstallStatus.notInstalled });
+ if (fromUpdate) {
+ // if there is an error during update, set it back to the previous version
+ // as handling of bad update is not implemented yet
+ setPackageInstallStatus({ ...currStatus, name });
+ } else {
+ setPackageInstallStatus({ name, status: InstallStatus.notInstalled, version });
+ }
notifications.toasts.addWarning({
title: toMountPoint(
{
- return packages[pkg].status;
- },
- [packages]
+ [getPackageInstallStatus, notifications.toasts, setPackageInstallStatus, toDetailView]
);
const uninstallPackage = useCallback(
async ({ name, version, title }: Pick) => {
- setPackageInstallStatus({ name, status: InstallStatus.uninstalling });
+ setPackageInstallStatus({ name, status: InstallStatus.uninstalling, version });
const pkgkey = `${name}-${version}`;
const res = await sendRemovePackage(pkgkey);
if (res.error) {
- setPackageInstallStatus({ name, status: InstallStatus.installed });
+ setPackageInstallStatus({ name, status: InstallStatus.installed, version });
notifications.toasts.addWarning({
title: toMountPoint(
;
export function ContentPanel(props: ContentPanelProps) {
- const { panel, name, version, assets, title, removable } = props;
+ const { panel, name, version, assets, title, removable, latestVersion } = props;
switch (panel) {
case 'settings':
return (
@@ -60,6 +60,7 @@ export function ContentPanel(props: ContentPanelProps) {
assets={assets}
title={title}
removable={removable}
+ latestVersion={latestVersion}
/>
);
case 'data-sources':
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx
index fa3245aec02c5..c82b7ed2297a7 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx
@@ -20,7 +20,7 @@ export const DataSourcesPanel = ({ name, version }: DataSourcesPanelProps) => {
const packageInstallStatus = getPackageInstallStatus(name);
// if they arrive at this page and the package is not installed, send them to overview
// this happens if they arrive with a direct url or they uninstall while on this tab
- if (packageInstallStatus !== InstallStatus.installed)
+ if (packageInstallStatus.status !== InstallStatus.installed)
return (
props.theme.eui.euiSizeM};
`;
-const StyledVersion = styled(Version)`
- font-size: ${props => props.theme.eui.euiFontSizeS};
- color: ${props => props.theme.eui.euiColorDarkShade};
-`;
-
type HeaderProps = PackageInfo & { iconType?: IconType };
export function Header(props: HeaderProps) {
- const { iconType, name, title, version } = props;
+ const { iconType, name, title, version, installedVersion, latestVersion } = props;
const hasWriteCapabilites = useCapabilities().write;
const { toListView } = useLinks();
const ADD_DATASOURCE_URI = useLink(`${EPM_PATH}/${name}-${version}/add-datasource`);
-
+ const updateAvailable = installedVersion && installedVersion < latestVersion ? true : false;
return (
@@ -59,7 +54,11 @@ export function Header(props: HeaderProps) {
{title}
-
+
+
+ {version} {updateAvailable && }
+
+
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx
index 3239d7b90e3c3..1f3eb2cc9362e 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx
@@ -32,11 +32,12 @@ export function Detail() {
const packageInfo = response.data?.response;
const title = packageInfo?.title;
const name = packageInfo?.name;
+ const installedVersion = packageInfo?.installedVersion;
const status: InstallStatus = packageInfo?.status as any;
// track install status state
if (name) {
- setPackageInstallStatus({ name, status });
+ setPackageInstallStatus({ name, status, version: installedVersion || null });
}
if (packageInfo) {
setInfo({ ...packageInfo, title: title || '' });
@@ -64,7 +65,6 @@ type LayoutProps = PackageInfo & Pick & Pick
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx
index cbbf1ce53c4ea..cdad67fd87548 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx
@@ -13,19 +13,21 @@ import { ConfirmPackageUninstall } from './confirm_package_uninstall';
import { ConfirmPackageInstall } from './confirm_package_install';
type InstallationButtonProps = Pick & {
- disabled: boolean;
+ disabled?: boolean;
+ isUpdate?: boolean;
};
export function InstallationButton(props: InstallationButtonProps) {
- const { assets, name, title, version, disabled = true } = props;
+ const { assets, name, title, version, disabled = true, isUpdate = false } = props;
const hasWriteCapabilites = useCapabilities().write;
const installPackage = useInstallPackage();
const uninstallPackage = useUninstallPackage();
const getPackageInstallStatus = useGetPackageInstallStatus();
- const installationStatus = getPackageInstallStatus(name);
+ const { status: installationStatus } = getPackageInstallStatus(name);
const isInstalling = installationStatus === InstallStatus.installing;
const isRemoving = installationStatus === InstallStatus.uninstalling;
const isInstalled = installationStatus === InstallStatus.installed;
+ const showUninstallButton = isInstalled || isRemoving;
const [isModalVisible, setModalVisible] = useState(false);
const toggleModal = useCallback(() => {
setModalVisible(!isModalVisible);
@@ -36,6 +38,10 @@ export function InstallationButton(props: InstallationButtonProps) {
toggleModal();
}, [installPackage, name, title, toggleModal, version]);
+ const handleClickUpdate = useCallback(() => {
+ installPackage({ name, version, title, fromUpdate: true });
+ }, [installPackage, name, title, version]);
+
const handleClickUninstall = useCallback(() => {
uninstallPackage({ name, version, title });
toggleModal();
@@ -78,6 +84,15 @@ export function InstallationButton(props: InstallationButtonProps) {
);
+ const updateButton = (
+
+
+
+ );
+
const uninstallButton = (
- {isInstalled || isRemoving ? uninstallButton : installButton}
+ {isUpdate ? updateButton : showUninstallButton ? uninstallButton : installButton}
{isModalVisible && (isInstalled ? uninstallModal : installModal)}
) : null;
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx
index 3589a1a9444e1..4d4dba2a64e5a 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx
@@ -8,11 +8,22 @@ import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { EuiSpacer } from '@elastic/eui';
+import styled from 'styled-components';
import { InstallStatus, PackageInfo } from '../../../../types';
import { useGetDatasources } from '../../../../hooks';
import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../constants';
import { useGetPackageInstallStatus } from '../../hooks';
import { InstallationButton } from './installation_button';
+import { UpdateIcon } from '../../components/icons';
+
+const SettingsTitleCell = styled.td`
+ padding-right: ${props => props.theme.eui.spacerSizes.xl};
+ padding-bottom: ${props => props.theme.eui.spacerSizes.m};
+`;
+
+const UpdatesAvailableMsgContainer = styled.span`
+ padding-left: ${props => props.theme.eui.spacerSizes.s};
+`;
const NoteLabel = () => (
(
defaultMessage="Note:"
/>
);
+const UpdatesAvailableMsg = () => (
+
+
+
+
+);
+
export const SettingsPanel = (
- props: Pick
+ props: Pick
) => {
+ const { name, title, removable, latestVersion, version } = props;
const getPackageInstallStatus = useGetPackageInstallStatus();
const { data: datasourcesData } = useGetDatasources({
perPage: 0,
page: 1,
kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name:${props.name}`,
});
- const { name, title, removable } = props;
- const packageInstallStatus = getPackageInstallStatus(name);
+ const { status: installationStatus, version: installedVersion } = getPackageInstallStatus(name);
const packageHasDatasources = !!datasourcesData?.total;
+ const updateAvailable = installedVersion && installedVersion < latestVersion ? true : false;
+ const isViewingOldPackage = version < latestVersion;
+ // hide install/remove options if the user has version of the package is installed
+ // and this package is out of date or if they do have a version installed but it's not this one
+ const hideInstallOptions =
+ (installationStatus === InstallStatus.notInstalled && isViewingOldPackage) ||
+ (installationStatus === InstallStatus.installed && installedVersion !== version);
+
+ const isUpdating = installationStatus === InstallStatus.installing && installedVersion;
return (
@@ -43,14 +73,13 @@ export const SettingsPanel = (
- {packageInstallStatus === InstallStatus.notInstalled ||
- packageInstallStatus === InstallStatus.installing ? (
+ {installedVersion !== null && (
-
-
-
+
+
+
+
+
+
+
+
+ {installedVersion}
+
+ {updateAvailable && }
+ |
+
+
+
+
+
+
+
+ {latestVersion}
+
+ |
+
+
+
+ {updateAvailable && (
+
+
+
+ )}
- ) : (
+ )}
+ {!hideInstallOptions && !isUpdating && (
-
-
+
+ {installationStatus === InstallStatus.notInstalled ||
+ installationStatus === InstallStatus.installing ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+ {packageHasDatasources && removable === true && (
+
+
+
+ ),
}}
/>
-
-
-
-
-
-
+
+ )}
+ {removable === false && (
+
+
+
+
+ ),
+ }}
+ />
+
+ )}
)}
-
-
-
-
-
-
-
- {packageHasDatasources && removable === true && (
-
-
-
-
- ),
- }}
- />
-
- )}
- {removable === false && (
-
-
-
-
- ),
- }}
- />
-
- )}
);
};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx
index 05729ccfc1af4..ab168ef1530bd 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx
@@ -37,7 +37,7 @@ export function SideNavLinks({ name, version, active }: NavLinkProps) {
: p.theme.eui.euiFontWeightRegular};
`;
// don't display Data Sources tab if the package is not installed
- if (packageInstallStatus !== InstallStatus.installed && panel === 'data-sources')
+ if (packageInstallStatus.status !== InstallStatus.installed && panel === 'data-sources')
return null;
return (
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx
index 05d150fd9ae23..70d8e7d6882f8 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx
@@ -8,6 +8,7 @@ import styled from 'styled-components';
import {
EuiButton,
EuiButtonEmpty,
+ EuiBetaBadge,
EuiPanel,
EuiText,
EuiTitle,
@@ -19,10 +20,11 @@ import {
EuiFlexItem,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
import { WithHeaderLayout } from '../../layouts';
import { useLink, useGetAgentConfigs } from '../../hooks';
import { AgentEnrollmentFlyout } from '../fleet/agent_list_page/components';
-import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH } from '../../constants';
+import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from '../../constants';
const OverviewPanel = styled(EuiPanel).attrs(props => ({
paddingSize: 'm',
@@ -57,6 +59,11 @@ const OverviewStats = styled(EuiDescriptionList).attrs(props => ({
}
`;
+const AlphaBadge = styled(EuiBetaBadge)`
+ vertical-align: top;
+ margin-left: ${props => props.theme.eui.paddingSizes.s};
+`;
+
export const IngestManagerOverview: React.FunctionComponent = () => {
// Agent enrollment flyout state
const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false);
@@ -79,6 +86,19 @@ export const IngestManagerOverview: React.FunctionComponent = () => {
id="xpack.ingestManager.overviewPageTitle"
defaultMessage="Ingest Manager"
/>
+
@@ -213,7 +233,7 @@ export const IngestManagerOverview: React.FunctionComponent = () => {
/>
-
+
+ (agentConfig: GetAgentConfigsResponseItem) =>
listAgents(soClient, {
showInactive: true,
perPage: 0,
diff --git a/x-pack/plugins/ingest_manager/server/saved_objects.ts b/x-pack/plugins/ingest_manager/server/saved_objects.ts
index 882258e859555..90fe68e61bb1b 100644
--- a/x-pack/plugins/ingest_manager/server/saved_objects.ts
+++ b/x-pack/plugins/ingest_manager/server/saved_objects.ts
@@ -128,6 +128,7 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = {
updated_on: { type: 'keyword' },
updated_by: { type: 'keyword' },
revision: { type: 'integer' },
+ monitoring_enabled: { type: 'keyword' },
},
},
},
diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.test.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.test.ts
new file mode 100644
index 0000000000000..17758f6e3d7f1
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/services/agent_config.test.ts
@@ -0,0 +1,134 @@
+/*
+ * 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 { savedObjectsClientMock } from 'src/core/server/mocks';
+import { agentConfigService } from './agent_config';
+import { Output } from '../types';
+
+function getSavedObjectMock(configAttributes: any) {
+ const mock = savedObjectsClientMock.create();
+
+ mock.get.mockImplementation(async (type: string, id: string) => {
+ return {
+ type,
+ id,
+ references: [],
+ attributes: configAttributes,
+ };
+ });
+
+ return mock;
+}
+
+jest.mock('./output', () => {
+ return {
+ outputService: {
+ getDefaultOutputId: () => 'test-id',
+ get: (): Output => {
+ return {
+ id: 'test-id',
+ is_default: true,
+ name: 'default',
+ // @ts-ignore
+ type: 'elasticsearch',
+ hosts: ['http://127.0.0.1:9201'],
+ };
+ },
+ },
+ };
+});
+
+describe('agent config', () => {
+ describe('getFullConfig', () => {
+ it('should return a config without monitoring if not monitoring is not enabled', async () => {
+ const soClient = getSavedObjectMock({
+ revision: 1,
+ });
+ const config = await agentConfigService.getFullConfig(soClient, 'config');
+
+ expect(config).toMatchObject({
+ id: 'config',
+ outputs: {
+ default: {
+ type: 'elasticsearch',
+ hosts: ['http://127.0.0.1:9201'],
+ ca_sha256: undefined,
+ api_key: undefined,
+ },
+ },
+ datasources: [],
+ revision: 1,
+ settings: {
+ monitoring: {
+ enabled: false,
+ logs: false,
+ metrics: false,
+ },
+ },
+ });
+ });
+
+ it('should return a config with monitoring if monitoring is enabled for logs', async () => {
+ const soClient = getSavedObjectMock({
+ revision: 1,
+ monitoring_enabled: ['logs'],
+ });
+ const config = await agentConfigService.getFullConfig(soClient, 'config');
+
+ expect(config).toMatchObject({
+ id: 'config',
+ outputs: {
+ default: {
+ type: 'elasticsearch',
+ hosts: ['http://127.0.0.1:9201'],
+ ca_sha256: undefined,
+ api_key: undefined,
+ },
+ },
+ datasources: [],
+ revision: 1,
+ settings: {
+ monitoring: {
+ use_output: 'default',
+ enabled: true,
+ logs: true,
+ metrics: false,
+ },
+ },
+ });
+ });
+
+ it('should return a config with monitoring if monitoring is enabled for metrics', async () => {
+ const soClient = getSavedObjectMock({
+ revision: 1,
+ monitoring_enabled: ['metrics'],
+ });
+ const config = await agentConfigService.getFullConfig(soClient, 'config');
+
+ expect(config).toMatchObject({
+ id: 'config',
+ outputs: {
+ default: {
+ type: 'elasticsearch',
+ hosts: ['http://127.0.0.1:9201'],
+ ca_sha256: undefined,
+ api_key: undefined,
+ },
+ },
+ datasources: [],
+ revision: 1,
+ settings: {
+ monitoring: {
+ use_output: 'default',
+ enabled: true,
+ logs: false,
+ metrics: true,
+ },
+ },
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.ts
index 75bbfc21293c2..7ab6ef1920c18 100644
--- a/x-pack/plugins/ingest_manager/server/services/agent_config.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts
@@ -301,28 +301,49 @@ class AgentConfigService {
if (!config) {
return null;
}
+ const defaultOutput = await outputService.get(
+ soClient,
+ await outputService.getDefaultOutputId(soClient)
+ );
const agentConfig: FullAgentConfig = {
id: config.id,
outputs: {
// TEMPORARY as we only support a default output
- ...[
- await outputService.get(soClient, await outputService.getDefaultOutputId(soClient)),
- ].reduce((outputs, { config: outputConfig, name, type, hosts, ca_sha256, api_key }) => {
- outputs[name] = {
- type,
- hosts,
- ca_sha256,
- api_key,
- ...outputConfig,
- };
- return outputs;
- }, {} as FullAgentConfig['outputs']),
+ ...[defaultOutput].reduce(
+ (outputs, { config: outputConfig, name, type, hosts, ca_sha256, api_key }) => {
+ outputs[name] = {
+ type,
+ hosts,
+ ca_sha256,
+ api_key,
+ ...outputConfig,
+ };
+ return outputs;
+ },
+ {} as FullAgentConfig['outputs']
+ ),
},
datasources: (config.datasources as Datasource[])
.filter(datasource => datasource.enabled)
.map(ds => storedDatasourceToAgentDatasource(ds)),
revision: config.revision,
+ ...(config.monitoring_enabled && config.monitoring_enabled.length > 0
+ ? {
+ settings: {
+ monitoring: {
+ use_output: defaultOutput.name,
+ enabled: true,
+ logs: config.monitoring_enabled.indexOf('logs') >= 0,
+ metrics: config.monitoring_enabled.indexOf('metrics') >= 0,
+ },
+ },
+ }
+ : {
+ settings: {
+ monitoring: { enabled: false, logs: false, metrics: false },
+ },
+ }),
};
return agentConfig;
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts
index d76584225877c..da8d79a04b97c 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts
@@ -67,9 +67,10 @@ export async function getPackageInfo(options: {
pkgVersion: string;
}): Promise {
const { savedObjectsClient, pkgName, pkgVersion } = options;
- const [item, savedObject, assets] = await Promise.all([
+ const [item, savedObject, latestPackage, assets] = await Promise.all([
Registry.fetchInfo(pkgName, pkgVersion),
getInstallationObject({ savedObjectsClient, pkgName }),
+ Registry.fetchFindLatestPackage(pkgName),
Registry.getArchiveInfo(pkgName, pkgVersion),
] as const);
// adding `as const` due to regression in TS 3.7.2
@@ -79,6 +80,7 @@ export async function getPackageInfo(options: {
// add properties that aren't (or aren't yet) on Registry response
const updated = {
...item,
+ latestVersion: latestPackage.version,
title: item.title || nameAsTitle(item.name),
assets: Registry.groupPathsByService(assets || []),
};
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts
index d49e0e661440f..c67cccd044bf5 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts
@@ -43,6 +43,7 @@ export function createInstallableFrom(
? {
...from,
status: InstallationStatus.installed,
+ installedVersion: savedObject.attributes.version,
savedObject,
}
: {
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
index 06f3decdbbe6f..8f51c4d78305c 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
@@ -106,7 +106,7 @@ export async function installPackage(options: {
try {
await deleteKibanaSavedObjectsAssets(savedObjectsClient, installedPkg.attributes.installed);
} catch (err) {
- // some assets may not exist if deleting during a failed update
+ // log these errors, some assets may not exist if deleted during a failed update
}
}
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts
index ac2c869f3b9e9..befb4722b6504 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts
@@ -121,8 +121,12 @@ export async function deleteKibanaSavedObjectsAssets(
const deletePromises = installedObjects.map(({ id, type }) => {
const assetType = type as AssetType;
if (savedObjectTypes.includes(assetType)) {
- savedObjectsClient.delete(assetType, id);
+ return savedObjectsClient.delete(assetType, id);
}
});
- await Promise.all(deletePromises);
+ try {
+ await Promise.all(deletePromises);
+ } catch (err) {
+ throw new Error('error deleting saved object asset');
+ }
}
diff --git a/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts b/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts
index 040b2eb16289a..59cadf3bd7f74 100644
--- a/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts
+++ b/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts
@@ -11,6 +11,9 @@ const AgentConfigBaseSchema = {
name: schema.string(),
namespace: schema.maybe(schema.string()),
description: schema.maybe(schema.string()),
+ monitoring_enabled: schema.maybe(
+ schema.arrayOf(schema.oneOf([schema.literal('logs'), schema.literal('metrics')]))
+ ),
};
export const NewAgentConfigSchema = schema.object({
diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx
index 359c06a6a9ebc..48729448b2ea5 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx
@@ -41,6 +41,10 @@ export const datatableVisualization: Visualization<
},
],
+ getVisualizationTypeId() {
+ return 'lnsDatatable';
+ },
+
getLayerIds(state) {
return state.layers.map(l => l.layerId);
},
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx
index 3c61d270b1bcf..c8d8064e60e38 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx
@@ -62,7 +62,25 @@ describe('chart_switch', () => {
id: 'subvisC2',
label: 'C2',
},
+ {
+ icon: 'empty',
+ id: 'subvisC3',
+ label: 'C3',
+ },
],
+ getSuggestions: jest.fn(options => {
+ if (options.subVisualizationId === 'subvisC2') {
+ return [];
+ }
+ return [
+ {
+ score: 1,
+ title: '',
+ state: `suggestion`,
+ previewIcon: 'empty',
+ },
+ ];
+ }),
},
};
}
@@ -313,10 +331,11 @@ describe('chart_switch', () => {
expect(getMenuItem('subvisB', component).prop('betaBadgeIconType')).toBeUndefined();
});
- it('should not indicate data loss if visualization is not changed', () => {
+ it('should not show a warning when the subvisualization is the same', () => {
const dispatch = jest.fn();
const frame = mockFrame(['a', 'b', 'c']);
const visualizations = mockVisualizations();
+ visualizations.visC.getVisualizationTypeId.mockReturnValue('subvisC2');
const switchVisualizationType = jest.fn(() => 'therebedragons');
visualizations.visC.switchVisualizationType = switchVisualizationType;
@@ -333,10 +352,10 @@ describe('chart_switch', () => {
/>
);
- expect(getMenuItem('subvisC2', component).prop('betaBadgeIconType')).toBeUndefined();
+ expect(getMenuItem('subvisC2', component).prop('betaBadgeIconType')).not.toBeDefined();
});
- it('should remove all layers if there is no suggestion', () => {
+ it('should get suggestions when switching subvisualization', () => {
const dispatch = jest.fn();
const visualizations = mockVisualizations();
visualizations.visB.getSuggestions.mockReturnValueOnce([]);
@@ -377,7 +396,7 @@ describe('chart_switch', () => {
const dispatch = jest.fn();
const frame = mockFrame(['a', 'b', 'c']);
const visualizations = mockVisualizations();
- const switchVisualizationType = jest.fn(() => 'therebedragons');
+ const switchVisualizationType = jest.fn(() => 'switched');
visualizations.visC.switchVisualizationType = switchVisualizationType;
@@ -393,12 +412,12 @@ describe('chart_switch', () => {
/>
);
- switchTo('subvisC2', component);
- expect(switchVisualizationType).toHaveBeenCalledWith('subvisC2', 'therebegriffins');
+ switchTo('subvisC3', component);
+ expect(switchVisualizationType).toHaveBeenCalledWith('subvisC3', 'suggestion');
expect(dispatch).toHaveBeenCalledWith(
expect.objectContaining({
type: 'SWITCH_VISUALIZATION',
- initialState: 'therebedragons',
+ initialState: 'switched',
})
);
expect(frame.removeLayers).not.toHaveBeenCalled();
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx
index 1461449f3c1c8..d73f83e75c0e4 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx
@@ -105,7 +105,16 @@ export function ChartSwitch(props: Props) {
const switchVisType =
props.visualizationMap[visualizationId].switchVisualizationType ||
((_type: string, initialState: unknown) => initialState);
- if (props.visualizationId === visualizationId) {
+ const layers = Object.entries(props.framePublicAPI.datasourceLayers);
+ const containsData = layers.some(
+ ([_layerId, datasource]) => datasource.getTableSpec().length > 0
+ );
+ // Always show the active visualization as a valid selection
+ if (
+ props.visualizationId === visualizationId &&
+ props.visualizationState &&
+ newVisualization.getVisualizationTypeId(props.visualizationState) === subVisualizationId
+ ) {
return {
visualizationId,
subVisualizationId,
@@ -116,13 +125,13 @@ export function ChartSwitch(props: Props) {
};
}
- const layers = Object.entries(props.framePublicAPI.datasourceLayers);
- const containsData = layers.some(
- ([_layerId, datasource]) => datasource.getTableSpec().length > 0
+ const topSuggestion = getTopSuggestion(
+ props,
+ visualizationId,
+ newVisualization,
+ subVisualizationId
);
- const topSuggestion = getTopSuggestion(props, visualizationId, newVisualization);
-
let dataLoss: VisualizationSelection['dataLoss'];
if (!containsData) {
@@ -250,7 +259,8 @@ export function ChartSwitch(props: Props) {
function getTopSuggestion(
props: Props,
visualizationId: string,
- newVisualization: Visualization
+ newVisualization: Visualization,
+ subVisualizationId?: string
): Suggestion | undefined {
const suggestions = getSuggestions({
datasourceMap: props.datasourceMap,
@@ -258,6 +268,7 @@ function getTopSuggestion(
visualizationMap: { [visualizationId]: newVisualization },
activeVisualizationId: props.visualizationId,
visualizationState: props.visualizationState,
+ subVisualizationId,
}).filter(suggestion => {
// don't use extended versions of current data table on switching between visualizations
// to avoid confusing the user.
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts
index eabcdfa7a24ab..949ae1f43448e 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts
@@ -44,6 +44,7 @@ export function getSuggestions({
datasourceStates,
visualizationMap,
activeVisualizationId,
+ subVisualizationId,
visualizationState,
field,
}: {
@@ -57,6 +58,7 @@ export function getSuggestions({
>;
visualizationMap: Record;
activeVisualizationId: string | null;
+ subVisualizationId?: string;
visualizationState: unknown;
field?: unknown;
}): Suggestion[] {
@@ -89,7 +91,8 @@ export function getSuggestions({
table,
visualizationId,
datasourceSuggestion,
- currentVisualizationState
+ currentVisualizationState,
+ subVisualizationId
);
})
)
@@ -108,13 +111,15 @@ function getVisualizationSuggestions(
table: TableSuggestion,
visualizationId: string,
datasourceSuggestion: DatasourceSuggestion & { datasourceId: string },
- currentVisualizationState: unknown
+ currentVisualizationState: unknown,
+ subVisualizationId?: string
) {
return visualization
.getSuggestions({
table,
state: currentVisualizationState,
keptLayerIds: datasourceSuggestion.keptLayerIds,
+ subVisualizationId,
})
.map(({ state, ...visualizationSuggestion }) => ({
...visualizationSuggestion,
diff --git a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx
index 50cd1ad8bd53a..e684fe8b3b5d6 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx
@@ -28,6 +28,7 @@ export function createMockVisualization(): jest.Mocked {
label: 'TEST',
},
],
+ getVisualizationTypeId: jest.fn(_state => 'empty'),
getDescription: jest.fn(_state => ({ label: '' })),
switchVisualizationType: jest.fn((_, x) => x),
getPersistableState: jest.fn(_state => _state),
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts
deleted file mode 100644
index 5f35ef650a08c..0000000000000
--- a/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
-import { getAutoDate } from './auto_date';
-
-describe('auto_date', () => {
- let autoDate: ReturnType;
-
- beforeEach(() => {
- autoDate = getAutoDate({ data: dataPluginMock.createSetupContract() });
- });
-
- it('should do nothing if no time range is provided', () => {
- const result = autoDate.fn(
- {
- type: 'kibana_context',
- },
- {
- aggConfigs: 'canttouchthis',
- },
- // eslint-disable-next-line
- {} as any
- );
-
- expect(result).toEqual('canttouchthis');
- });
-
- it('should not change anything if there are no auto date histograms', () => {
- const aggConfigs = JSON.stringify([
- { type: 'date_histogram', params: { interval: '35h' } },
- { type: 'count' },
- ]);
- const result = autoDate.fn(
- {
- timeRange: {
- from: 'now-10d',
- to: 'now',
- },
- type: 'kibana_context',
- },
- {
- aggConfigs,
- },
- // eslint-disable-next-line
- {} as any
- );
-
- expect(result).toEqual(aggConfigs);
- });
-
- it('should change auto date histograms', () => {
- const aggConfigs = JSON.stringify([
- { type: 'date_histogram', params: { interval: 'auto' } },
- { type: 'count' },
- ]);
- const result = autoDate.fn(
- {
- timeRange: {
- from: 'now-10d',
- to: 'now',
- },
- type: 'kibana_context',
- },
- {
- aggConfigs,
- },
- // eslint-disable-next-line
- {} as any
- );
-
- const interval = JSON.parse(result).find(
- (agg: { type: string }) => agg.type === 'date_histogram'
- ).params.interval;
-
- expect(interval).toBeTruthy();
- expect(typeof interval).toEqual('string');
- expect(interval).not.toEqual('auto');
- });
-});
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts b/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts
deleted file mode 100644
index 97a46f4a3e176..0000000000000
--- a/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public';
-import {
- ExpressionFunctionDefinition,
- KibanaContext,
-} from '../../../../../src/plugins/expressions/public';
-
-interface LensAutoDateProps {
- aggConfigs: string;
-}
-
-export function getAutoDate(deps: {
- data: DataPublicPluginSetup;
-}): ExpressionFunctionDefinition<
- 'lens_auto_date',
- KibanaContext | null,
- LensAutoDateProps,
- string
-> {
- function autoIntervalFromContext(ctx?: KibanaContext | null) {
- if (!ctx || !ctx.timeRange) {
- return;
- }
-
- return deps.data.search.aggs.calculateAutoTimeExpression(ctx.timeRange);
- }
-
- /**
- * Convert all 'auto' date histograms into a concrete value (e.g. 2h).
- * This allows us to support 'auto' on all date fields, and opens the
- * door to future customizations (e.g. adjusting the level of detail, etc).
- */
- return {
- name: 'lens_auto_date',
- aliases: [],
- help: '',
- inputTypes: ['kibana_context', 'null'],
- args: {
- aggConfigs: {
- types: ['string'],
- default: '""',
- help: '',
- },
- },
- fn(input, args) {
- const interval = autoIntervalFromContext(input);
-
- if (!interval) {
- return args.aggConfigs;
- }
-
- const configs = JSON.parse(args.aggConfigs) as Array<{
- type: string;
- params: { interval: string };
- }>;
-
- const updatedConfigs = configs.map(c => {
- if (c.type !== 'date_histogram' || !c.params || c.params.interval !== 'auto') {
- return c;
- }
-
- return {
- ...c,
- params: {
- ...c.params,
- interval,
- },
- };
- });
-
- return JSON.stringify(updatedConfigs);
- },
- };
-}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts
index fe14f472341af..73fd144b9c7f8 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts
@@ -8,7 +8,6 @@ import { CoreSetup } from 'kibana/public';
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
import { getIndexPatternDatasource } from './indexpattern';
import { renameColumns } from './rename_columns';
-import { getAutoDate } from './auto_date';
import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public';
import {
DataPublicPluginSetup,
@@ -31,10 +30,9 @@ export class IndexPatternDatasource {
setup(
core: CoreSetup,
- { data: dataSetup, expressions, editorFrame }: IndexPatternDatasourceSetupPlugins
+ { expressions, editorFrame }: IndexPatternDatasourceSetupPlugins
) {
expressions.registerFunction(renameColumns);
- expressions.registerFunction(getAutoDate({ data: dataSetup }));
editorFrame.registerDatasource(
core.getStartServices().then(([coreStart, { data }]) =>
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
index e4f3677d0fe88..06635e663361d 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
@@ -10,6 +10,7 @@ import { DatasourcePublicAPI, Operation, Datasource } from '../types';
import { coreMock } from 'src/core/public/mocks';
import { IndexPatternPersistedState, IndexPatternPrivateState } from './types';
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
+import { Ast } from '@kbn/interpreter/common';
jest.mock('./loader');
jest.mock('../id_generator');
@@ -262,20 +263,7 @@ describe('IndexPattern Data Source', () => {
Object {
"arguments": Object {
"aggConfigs": Array [
- Object {
- "chain": Array [
- Object {
- "arguments": Object {
- "aggConfigs": Array [
- "[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1d\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}}]",
- ],
- },
- "function": "lens_auto_date",
- "type": "function",
- },
- ],
- "type": "expression",
- },
+ "[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1d\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}}]",
],
"includeFormatHints": Array [
true,
@@ -289,6 +277,9 @@ describe('IndexPattern Data Source', () => {
"partialRows": Array [
false,
],
+ "timeFields": Array [
+ "timestamp",
+ ],
},
"function": "esaggs",
"type": "function",
@@ -307,6 +298,89 @@ describe('IndexPattern Data Source', () => {
}
`);
});
+
+ it('should put all time fields used in date_histograms to the esaggs timeFields parameter', async () => {
+ const queryPersistedState: IndexPatternPersistedState = {
+ currentIndexPatternId: '1',
+ layers: {
+ first: {
+ indexPatternId: '1',
+ columnOrder: ['col1', 'col2', 'col3'],
+ columns: {
+ col1: {
+ label: 'Count of records',
+ dataType: 'number',
+ isBucketed: false,
+ sourceField: 'Records',
+ operationType: 'count',
+ },
+ col2: {
+ label: 'Date',
+ dataType: 'date',
+ isBucketed: true,
+ operationType: 'date_histogram',
+ sourceField: 'timestamp',
+ params: {
+ interval: 'auto',
+ },
+ },
+ col3: {
+ label: 'Date 2',
+ dataType: 'date',
+ isBucketed: true,
+ operationType: 'date_histogram',
+ sourceField: 'another_datefield',
+ params: {
+ interval: 'auto',
+ },
+ },
+ },
+ },
+ },
+ };
+
+ const state = stateFromPersistedState(queryPersistedState);
+
+ const ast = indexPatternDatasource.toExpression(state, 'first') as Ast;
+ expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp', 'another_datefield']);
+ });
+
+ it('should not put date fields used outside date_histograms to the esaggs timeFields parameter', async () => {
+ const queryPersistedState: IndexPatternPersistedState = {
+ currentIndexPatternId: '1',
+ layers: {
+ first: {
+ indexPatternId: '1',
+ columnOrder: ['col1', 'col2'],
+ columns: {
+ col1: {
+ label: 'Count of records',
+ dataType: 'date',
+ isBucketed: false,
+ sourceField: 'timefield',
+ operationType: 'cardinality',
+ },
+ col2: {
+ label: 'Date',
+ dataType: 'date',
+ isBucketed: true,
+ operationType: 'date_histogram',
+ sourceField: 'timestamp',
+ params: {
+ interval: 'auto',
+ },
+ },
+ },
+ },
+ },
+ };
+
+ const state = stateFromPersistedState(queryPersistedState);
+
+ const ast = indexPatternDatasource.toExpression(state, 'first') as Ast;
+ expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp']);
+ expect(ast.chain[0].arguments.timeFields).not.toContain('timefield');
+ });
});
describe('#insertLayer', () => {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts
index 3ab51b5fa3f2b..1308fa3b7ca60 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts
@@ -10,6 +10,7 @@ import { IndexPatternColumn } from './indexpattern';
import { operationDefinitionMap } from './operations';
import { IndexPattern, IndexPatternPrivateState } from './types';
import { OriginalColumn } from './rename_columns';
+import { dateHistogramOperation } from './operations/definitions';
function getExpressionForLayer(
indexPattern: IndexPattern,
@@ -68,6 +69,12 @@ function getExpressionForLayer(
return base;
});
+ const allDateHistogramFields = Object.values(columns)
+ .map(column =>
+ column.operationType === dateHistogramOperation.type ? column.sourceField : null
+ )
+ .filter((field): field is string => Boolean(field));
+
return {
type: 'expression',
chain: [
@@ -79,20 +86,8 @@ function getExpressionForLayer(
metricsAtAllLevels: [false],
partialRows: [false],
includeFormatHints: [true],
- aggConfigs: [
- {
- type: 'expression',
- chain: [
- {
- type: 'function',
- function: 'lens_auto_date',
- arguments: {
- aggConfigs: [JSON.stringify(aggs)],
- },
- },
- ],
- },
- ],
+ timeFields: allDateHistogramFields,
+ aggConfigs: [JSON.stringify(aggs)],
},
},
{
diff --git a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx
index 73b8019a31eaa..04a1c3865f22d 100644
--- a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx
+++ b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx
@@ -53,6 +53,10 @@ export const metricVisualization: Visualization = {
},
],
+ getVisualizationTypeId() {
+ return 'lnsMetric';
+ },
+
clearLayer(state) {
return {
...state,
diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts
index 181f192520d0d..ed0af8545f012 100644
--- a/x-pack/plugins/lens/public/types.ts
+++ b/x-pack/plugins/lens/public/types.ts
@@ -312,6 +312,10 @@ export interface SuggestionRequest {
* The visualization needs to know which table is being suggested
*/
keptLayerIds: string[];
+ /**
+ * Different suggestions can be generated for each subtype of the visualization
+ */
+ subVisualizationId?: string;
}
/**
@@ -388,6 +392,11 @@ export interface Visualization {
* but can register multiple subtypes
*/
visualizationTypes: VisualizationType[];
+ /**
+ * Return the ID of the current visualization. Used to highlight
+ * the active subtype of the visualization.
+ */
+ getVisualizationTypeId: (state: T) => string;
/**
* If the visualization has subtypes, update the subtype in state.
*/
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts
index beccf0dc46eb4..d176905c65120 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts
@@ -27,7 +27,7 @@ function exampleState(): State {
}
describe('xy_visualization', () => {
- describe('getDescription', () => {
+ describe('#getDescription', () => {
function mixedState(...types: SeriesType[]) {
const state = exampleState();
return {
@@ -81,6 +81,45 @@ describe('xy_visualization', () => {
});
});
+ describe('#getVisualizationTypeId', () => {
+ function mixedState(...types: SeriesType[]) {
+ const state = exampleState();
+ return {
+ ...state,
+ layers: types.map((t, i) => ({
+ ...state.layers[0],
+ layerId: `layer_${i}`,
+ seriesType: t,
+ })),
+ };
+ }
+
+ it('should show mixed when each layer is different', () => {
+ expect(xyVisualization.getVisualizationTypeId(mixedState('bar', 'line'))).toEqual('mixed');
+ });
+
+ it('should show the preferredSeriesType if there are no layers', () => {
+ expect(xyVisualization.getVisualizationTypeId(mixedState())).toEqual('bar');
+ });
+
+ it('should combine multiple layers into one type', () => {
+ expect(
+ xyVisualization.getVisualizationTypeId(mixedState('bar_horizontal', 'bar_horizontal'))
+ ).toEqual('bar_horizontal');
+ });
+
+ it('should return the subtype for single layers', () => {
+ expect(xyVisualization.getVisualizationTypeId(mixedState('area'))).toEqual('area');
+ expect(xyVisualization.getVisualizationTypeId(mixedState('line'))).toEqual('line');
+ expect(xyVisualization.getVisualizationTypeId(mixedState('area_stacked'))).toEqual(
+ 'area_stacked'
+ );
+ expect(xyVisualization.getVisualizationTypeId(mixedState('bar_horizontal_stacked'))).toEqual(
+ 'bar_horizontal_stacked'
+ );
+ });
+ });
+
describe('#initialize', () => {
it('loads default state', () => {
const mockFrame = createMockFramePublicAPI();
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx
index c72fa0fec24d7..e91edf9cc0183 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx
@@ -12,7 +12,7 @@ import { I18nProvider } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { getSuggestions } from './xy_suggestions';
import { LayerContextMenu } from './xy_config_panel';
-import { Visualization, OperationMetadata } from '../types';
+import { Visualization, OperationMetadata, VisualizationType } from '../types';
import { State, PersistableState, SeriesType, visualizationTypes, LayerConfig } from './types';
import { toExpression, toPreviewExpression } from './to_expression';
import chartBarStackedSVG from '../assets/chart_bar_stacked.svg';
@@ -24,6 +24,18 @@ const defaultSeriesType = 'bar_stacked';
const isNumericMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number';
const isBucketed = (op: OperationMetadata) => op.isBucketed;
+function getVisualizationType(state: State): VisualizationType | 'mixed' {
+ if (!state.layers.length) {
+ return (
+ visualizationTypes.find(t => t.id === state.preferredSeriesType) ?? visualizationTypes[0]
+ );
+ }
+ const visualizationType = visualizationTypes.find(t => t.id === state.layers[0].seriesType);
+ const seriesTypes = _.unique(state.layers.map(l => l.seriesType));
+
+ return visualizationType && seriesTypes.length === 1 ? visualizationType : 'mixed';
+}
+
function getDescription(state?: State) {
if (!state) {
return {
@@ -34,32 +46,31 @@ function getDescription(state?: State) {
};
}
+ const visualizationType = getVisualizationType(state);
+
if (!state.layers.length) {
- const visualizationType = visualizationTypes.find(v => v.id === state.preferredSeriesType)!;
+ const preferredType = visualizationType as VisualizationType;
return {
- icon: visualizationType.largeIcon || visualizationType.icon,
- label: visualizationType.label,
+ icon: preferredType.largeIcon || preferredType.icon,
+ label: preferredType.label,
};
}
- const visualizationType = visualizationTypes.find(t => t.id === state.layers[0].seriesType)!;
- const seriesTypes = _.unique(state.layers.map(l => l.seriesType));
-
return {
icon:
- seriesTypes.length === 1
- ? visualizationType.largeIcon || visualizationType.icon
- : chartMixedSVG,
+ visualizationType === 'mixed'
+ ? chartMixedSVG
+ : visualizationType.largeIcon || visualizationType.icon,
label:
- seriesTypes.length === 1
- ? visualizationType.label
- : isHorizontalChart(state.layers)
- ? i18n.translate('xpack.lens.xyVisualization.mixedBarHorizontalLabel', {
- defaultMessage: 'Mixed horizontal bar',
- })
- : i18n.translate('xpack.lens.xyVisualization.mixedLabel', {
- defaultMessage: 'Mixed XY',
- }),
+ visualizationType === 'mixed'
+ ? isHorizontalChart(state.layers)
+ ? i18n.translate('xpack.lens.xyVisualization.mixedBarHorizontalLabel', {
+ defaultMessage: 'Mixed horizontal bar',
+ })
+ : i18n.translate('xpack.lens.xyVisualization.mixedLabel', {
+ defaultMessage: 'Mixed XY',
+ })
+ : visualizationType.label,
};
}
@@ -67,6 +78,10 @@ export const xyVisualization: Visualization = {
id: 'lnsXY',
visualizationTypes,
+ getVisualizationTypeId(state) {
+ const type = getVisualizationType(state);
+ return type === 'mixed' ? type : type.id;
+ },
getLayerIds(state) {
return state.layers.map(l => l.layerId);
diff --git a/x-pack/plugins/lens/server/migrations.test.ts b/x-pack/plugins/lens/server/migrations.test.ts
index e80308cc9acdb..4cc330d40efd7 100644
--- a/x-pack/plugins/lens/server/migrations.test.ts
+++ b/x-pack/plugins/lens/server/migrations.test.ts
@@ -158,4 +158,124 @@ describe('Lens migrations', () => {
]);
});
});
+
+ describe('7.8.0 auto timestamp', () => {
+ const context = {} as SavedObjectMigrationContext;
+
+ const example = {
+ type: 'lens',
+ attributes: {
+ expression: `kibana
+ | kibana_context query="{\\"query\\":\\"\\",\\"language\\":\\"kuery\\"}" filters="[]"
+ | lens_merge_tables layerIds="bd09dc71-a7e2-42d0-83bd-85df8291f03c"
+ tables={esaggs
+ index="ff959d40-b880-11e8-a6d9-e546fe2bba5f"
+ metricsAtAllLevels=false
+ partialRows=false
+ includeFormatHints=true
+ aggConfigs={
+ lens_auto_date
+ aggConfigs="[{\\"id\\":\\"1d9cc16c-1460-41de-88f8-471932ecbc97\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"products.created_on\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"auto\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}},{\\"id\\":\\"66115819-8481-4917-a6dc-8ffb10dd02df\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}}]"
+ }
+ | lens_rename_columns idMap="{\\"col-0-1d9cc16c-1460-41de-88f8-471932ecbc97\\":{\\"label\\":\\"products.created_on\\",\\"dataType\\":\\"date\\",\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"products.created_on\\",\\"isBucketed\\":true,\\"scale\\":\\"interval\\",\\"params\\":{\\"interval\\":\\"auto\\"},\\"id\\":\\"1d9cc16c-1460-41de-88f8-471932ecbc97\\"},\\"col-1-66115819-8481-4917-a6dc-8ffb10dd02df\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"operationType\\":\\"count\\",\\"suggestedPriority\\":0,\\"isBucketed\\":false,\\"scale\\":\\"ratio\\",\\"sourceField\\":\\"Records\\",\\"id\\":\\"66115819-8481-4917-a6dc-8ffb10dd02df\\"}}"
+ }
+ | lens_xy_chart
+ xTitle="products.created_on"
+ yTitle="Count of records"
+ legend={lens_xy_legendConfig isVisible=true position="right"}
+ layers={lens_xy_layer
+ layerId="bd09dc71-a7e2-42d0-83bd-85df8291f03c"
+ hide=false
+ xAccessor="1d9cc16c-1460-41de-88f8-471932ecbc97"
+ yScaleType="linear"
+ xScaleType="time"
+ isHistogram=true
+ seriesType="bar_stacked"
+ accessors="66115819-8481-4917-a6dc-8ffb10dd02df"
+ columnToLabel="{\\"66115819-8481-4917-a6dc-8ffb10dd02df\\":\\"Count of records\\"}"
+ }
+ `,
+ state: {
+ datasourceStates: {
+ indexpattern: {
+ currentIndexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
+ layers: {
+ 'bd09dc71-a7e2-42d0-83bd-85df8291f03c': {
+ indexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
+ columns: {
+ '1d9cc16c-1460-41de-88f8-471932ecbc97': {
+ label: 'products.created_on',
+ dataType: 'date',
+ operationType: 'date_histogram',
+ sourceField: 'products.created_on',
+ isBucketed: true,
+ scale: 'interval',
+ params: { interval: 'auto' },
+ },
+ '66115819-8481-4917-a6dc-8ffb10dd02df': {
+ label: 'Count of records',
+ dataType: 'number',
+ operationType: 'count',
+ suggestedPriority: 0,
+ isBucketed: false,
+ scale: 'ratio',
+ sourceField: 'Records',
+ },
+ },
+ columnOrder: [
+ '1d9cc16c-1460-41de-88f8-471932ecbc97',
+ '66115819-8481-4917-a6dc-8ffb10dd02df',
+ ],
+ },
+ },
+ },
+ },
+ datasourceMetaData: {
+ filterableIndexPatterns: [
+ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', title: 'kibana_sample_data_ecommerce' },
+ ],
+ },
+ visualization: {
+ legend: { isVisible: true, position: 'right' },
+ preferredSeriesType: 'bar_stacked',
+ layers: [
+ {
+ layerId: 'bd09dc71-a7e2-42d0-83bd-85df8291f03c',
+ accessors: ['66115819-8481-4917-a6dc-8ffb10dd02df'],
+ position: 'top',
+ seriesType: 'bar_stacked',
+ showGridlines: false,
+ xAccessor: '1d9cc16c-1460-41de-88f8-471932ecbc97',
+ },
+ ],
+ },
+ query: { query: '', language: 'kuery' },
+ filters: [],
+ },
+ title: 'Bar chart',
+ visualizationType: 'lnsXY',
+ },
+ };
+
+ it('should remove the lens_auto_date expression', () => {
+ const result = migrations['7.8.0'](example, context);
+ expect(result.attributes.expression).toContain(`timeFields=\"products.created_on\"`);
+ });
+
+ it('should handle pre-migrated expression', () => {
+ const input = {
+ type: 'lens',
+ attributes: {
+ ...example.attributes,
+ expression: `kibana
+| kibana_context query="{\\"query\\":\\"\\",\\"language\\":\\"kuery\\"}" filters="[]"
+| lens_merge_tables layerIds="bd09dc71-a7e2-42d0-83bd-85df8291f03c"
+ tables={esaggs index="ff959d40-b880-11e8-a6d9-e546fe2bba5f" metricsAtAllLevels=false partialRows=false includeFormatHints=true aggConfigs="[{\\"id\\":\\"1d9cc16c-1460-41de-88f8-471932ecbc97\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"products.created_on\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"auto\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}},{\\"id\\":\\"66115819-8481-4917-a6dc-8ffb10dd02df\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}}]" timeFields=\"products.created_on\"}
+| lens_xy_chart xTitle="products.created_on" yTitle="Count of records" legend={lens_xy_legendConfig isVisible=true position="right"} layers={}`,
+ },
+ };
+ const result = migrations['7.8.0'](input, context);
+ expect(result).toEqual(input);
+ });
+ });
});
diff --git a/x-pack/plugins/lens/server/migrations.ts b/x-pack/plugins/lens/server/migrations.ts
index 3d238723b7438..51fcd3b6198c3 100644
--- a/x-pack/plugins/lens/server/migrations.ts
+++ b/x-pack/plugins/lens/server/migrations.ts
@@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { cloneDeep } from 'lodash';
+import { cloneDeep, flow } from 'lodash';
+import { fromExpression, toExpression, Ast, ExpressionFunctionAST } from '@kbn/interpreter/common';
import { SavedObjectMigrationFn } from 'src/core/server';
interface XYLayerPre77 {
@@ -14,6 +15,122 @@ interface XYLayerPre77 {
accessors: string[];
}
+/**
+ * Removes the `lens_auto_date` subexpression from a stored expression
+ * string. For example: aggConfigs={lens_auto_date aggConfigs="JSON string"}
+ */
+const removeLensAutoDate: SavedObjectMigrationFn = (doc, context) => {
+ const expression: string = doc.attributes?.expression;
+ try {
+ const ast = fromExpression(expression);
+ const newChain: ExpressionFunctionAST[] = ast.chain.map(topNode => {
+ if (topNode.function !== 'lens_merge_tables') {
+ return topNode;
+ }
+ return {
+ ...topNode,
+ arguments: {
+ ...topNode.arguments,
+ tables: (topNode.arguments.tables as Ast[]).map(middleNode => {
+ return {
+ type: 'expression',
+ chain: middleNode.chain.map(node => {
+ // Check for sub-expression in aggConfigs
+ if (
+ node.function === 'esaggs' &&
+ typeof node.arguments.aggConfigs[0] !== 'string'
+ ) {
+ return {
+ ...node,
+ arguments: {
+ ...node.arguments,
+ aggConfigs: (node.arguments.aggConfigs[0] as Ast).chain[0].arguments
+ .aggConfigs,
+ },
+ };
+ }
+ return node;
+ }),
+ };
+ }),
+ },
+ };
+ });
+
+ return {
+ ...doc,
+ attributes: {
+ ...doc.attributes,
+ expression: toExpression({ ...ast, chain: newChain }),
+ },
+ };
+ } catch (e) {
+ context.log.warning(e.message);
+ return { ...doc };
+ }
+};
+
+/**
+ * Adds missing timeField arguments to esaggs in the Lens expression
+ */
+const addTimeFieldToEsaggs: SavedObjectMigrationFn = (doc, context) => {
+ const expression: string = doc.attributes?.expression;
+
+ try {
+ const ast = fromExpression(expression);
+ const newChain: ExpressionFunctionAST[] = ast.chain.map(topNode => {
+ if (topNode.function !== 'lens_merge_tables') {
+ return topNode;
+ }
+ return {
+ ...topNode,
+ arguments: {
+ ...topNode.arguments,
+ tables: (topNode.arguments.tables as Ast[]).map(middleNode => {
+ return {
+ type: 'expression',
+ chain: middleNode.chain.map(node => {
+ // Skip if there are any timeField arguments already, because that indicates
+ // the fix is already applied
+ if (node.function !== 'esaggs' || node.arguments.timeFields) {
+ return node;
+ }
+ const timeFields: string[] = [];
+ JSON.parse(node.arguments.aggConfigs[0] as string).forEach(
+ (agg: { type: string; params: { field: string } }) => {
+ if (agg.type !== 'date_histogram') {
+ return;
+ }
+ timeFields.push(agg.params.field);
+ }
+ );
+ return {
+ ...node,
+ arguments: {
+ ...node.arguments,
+ timeFields,
+ },
+ };
+ }),
+ };
+ }),
+ },
+ };
+ });
+
+ return {
+ ...doc,
+ attributes: {
+ ...doc.attributes,
+ expression: toExpression({ ...ast, chain: newChain }),
+ },
+ };
+ } catch (e) {
+ context.log.warning(e.message);
+ return { ...doc };
+ }
+};
+
export const migrations: Record = {
'7.7.0': doc => {
const newDoc = cloneDeep(doc);
@@ -34,4 +151,7 @@ export const migrations: Record = {
}
return newDoc;
},
+ // The order of these migrations matter, since the timefield migration relies on the aggConfigs
+ // sitting directly on the esaggs as an argument and not a nested function (which lens_auto_date was).
+ '7.8.0': flow(removeLensAutoDate, addTimeFieldToEsaggs),
};
diff --git a/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts b/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts
index 633e8c86d8c94..7715541b1c52d 100644
--- a/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts
+++ b/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts
@@ -20,6 +20,7 @@ export type RenderWizardArguments = {
};
export type LayerWizard = {
+ checkVisibility?: () => boolean;
description: string;
icon: string;
isIndexingSource?: boolean;
@@ -34,5 +35,7 @@ export function registerLayerWizard(layerWizard: LayerWizard) {
}
export function getLayerWizards(): LayerWizard[] {
- return [...registry];
+ return registry.filter(layerWizard => {
+ return layerWizard.checkVisibility ? layerWizard.checkVisibility() : true;
+ });
}
diff --git a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx
index f31e770df2d95..a6e2e7f42657c 100644
--- a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx
@@ -12,8 +12,13 @@ import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'
import { EMSFileCreateSourceEditor } from './create_source_editor';
// @ts-ignore
import { EMSFileSource, sourceTitle } from './ems_file_source';
+// @ts-ignore
+import { isEmsEnabled } from '../../../meta';
export const emsBoundariesLayerWizardConfig: LayerWizard = {
+ checkVisibility: () => {
+ return isEmsEnabled();
+ },
description: i18n.translate('xpack.maps.source.emsFileDescription', {
defaultMessage: 'Administrative boundaries from Elastic Maps Service',
}),
diff --git a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx
index ced33a0bcf84a..fc745edbabee8 100644
--- a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx
@@ -12,8 +12,13 @@ import { EMSTMSSource, sourceTitle } from './ems_tms_source';
import { VectorTileLayer } from '../../vector_tile_layer';
// @ts-ignore
import { TileServiceSelect } from './tile_service_select';
+// @ts-ignore
+import { isEmsEnabled } from '../../../meta';
export const emsBaseMapLayerWizardConfig: LayerWizard = {
+ checkVisibility: () => {
+ return isEmsEnabled();
+ },
description: i18n.translate('xpack.maps.source.emsTileDescription', {
defaultMessage: 'Tile map service from Elastic Maps Service',
}),
diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx
index 4321501760faf..a9adec2bda2c8 100644
--- a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx
@@ -12,8 +12,14 @@ import { KibanaRegionmapSource, sourceTitle } from './kibana_regionmap_source';
import { VectorLayer } from '../../vector_layer';
// @ts-ignore
import { CreateSourceEditor } from './create_source_editor';
+// @ts-ignore
+import { getKibanaRegionList } from '../../../meta';
export const kibanaRegionMapLayerWizardConfig: LayerWizard = {
+ checkVisibility: () => {
+ const regions = getKibanaRegionList();
+ return regions.length;
+ },
description: i18n.translate('xpack.maps.source.kbnRegionMapDescription', {
defaultMessage: 'Vector data from hosted GeoJSON configured in kibana.yml',
}),
diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx
index aeea2d6084f84..141fabeedd3e5 100644
--- a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx
@@ -12,8 +12,14 @@ import { CreateSourceEditor } from './create_source_editor';
// @ts-ignore
import { KibanaTilemapSource, sourceTitle } from './kibana_tilemap_source';
import { TileLayer } from '../../tile_layer';
+// @ts-ignore
+import { getKibanaTileMap } from '../../../meta';
export const kibanaBasemapLayerWizardConfig: LayerWizard = {
+ checkVisibility: () => {
+ const tilemap = getKibanaTileMap();
+ return !!tilemap.url;
+ },
description: i18n.translate('xpack.maps.source.kbnTMSDescription', {
defaultMessage: 'Tile map service configured in kibana.yml',
}),
diff --git a/x-pack/plugins/maps/public/meta.js b/x-pack/plugins/maps/public/meta.js
index d4612554cf00b..c3245e8e98db2 100644
--- a/x-pack/plugins/maps/public/meta.js
+++ b/x-pack/plugins/maps/public/meta.js
@@ -36,12 +36,15 @@ function fetchFunction(...args) {
return fetch(...args);
}
+export function isEmsEnabled() {
+ return getInjectedVarFunc()('isEmsEnabled', true);
+}
+
let emsClient = null;
let latestLicenseId = null;
export function getEMSClient() {
if (!emsClient) {
- const isEmsEnabled = getInjectedVarFunc()('isEmsEnabled', true);
- if (isEmsEnabled) {
+ if (isEmsEnabled()) {
const proxyElasticMapsServiceInMaps = getInjectedVarFunc()(
'proxyElasticMapsServiceInMaps',
false
@@ -86,7 +89,7 @@ export function getEMSClient() {
}
export function getGlyphUrl() {
- if (!getInjectedVarFunc()('isEmsEnabled', true)) {
+ if (!isEmsEnabled()) {
return '';
}
return getInjectedVarFunc()('proxyElasticMapsServiceInMaps', false)
diff --git a/x-pack/plugins/siem/public/lib/telemetry/middleware.ts b/x-pack/plugins/siem/public/lib/telemetry/middleware.ts
index 59c6cb3566907..ca889e20e695f 100644
--- a/x-pack/plugins/siem/public/lib/telemetry/middleware.ts
+++ b/x-pack/plugins/siem/public/lib/telemetry/middleware.ts
@@ -7,7 +7,7 @@
import { Action, Dispatch, MiddlewareAPI } from 'redux';
import { track, METRIC_TYPE, TELEMETRY_EVENT } from './';
-import { timelineActions } from '../../store/actions';
+import * as timelineActions from '../../store/timeline/actions';
export const telemetryMiddleware = (api: MiddlewareAPI) => (next: Dispatch) => (action: Action) => {
if (timelineActions.endTimelineSaving.match(action)) {
diff --git a/x-pack/plugins/siem/public/store/model.ts b/x-pack/plugins/siem/public/store/model.ts
index 9e9e663a59fe0..686dc096e61b0 100644
--- a/x-pack/plugins/siem/public/store/model.ts
+++ b/x-pack/plugins/siem/public/store/model.ts
@@ -9,15 +9,4 @@ export { dragAndDropModel } from './drag_and_drop';
export { hostsModel } from './hosts';
export { inputsModel } from './inputs';
export { networkModel } from './network';
-
-export type KueryFilterQueryKind = 'kuery' | 'lucene';
-
-export interface KueryFilterQuery {
- kind: KueryFilterQueryKind;
- expression: string;
-}
-
-export interface SerializedFilterQuery {
- kuery: KueryFilterQuery | null;
- serializedQuery: string;
-}
+export * from './types';
diff --git a/x-pack/plugins/siem/public/store/timeline/actions.ts b/x-pack/plugins/siem/public/store/timeline/actions.ts
index a03cc2643e014..12155decf40d4 100644
--- a/x-pack/plugins/siem/public/store/timeline/actions.ts
+++ b/x-pack/plugins/siem/public/store/timeline/actions.ts
@@ -12,7 +12,7 @@ import {
DataProvider,
QueryOperator,
} from '../../components/timeline/data_providers/data_provider';
-import { KueryFilterQuery, SerializedFilterQuery } from '../model';
+import { KueryFilterQuery, SerializedFilterQuery } from '../types';
import { EventType, KqlMode, TimelineModel, ColumnHeaderOptions } from './model';
import { TimelineNonEcsData } from '../../graphql/types';
diff --git a/x-pack/plugins/siem/public/store/types.ts b/x-pack/plugins/siem/public/store/types.ts
new file mode 100644
index 0000000000000..2c679ba41116e
--- /dev/null
+++ b/x-pack/plugins/siem/public/store/types.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export type KueryFilterQueryKind = 'kuery' | 'lucene';
+
+export interface KueryFilterQuery {
+ kind: KueryFilterQueryKind;
+ expression: string;
+}
+
+export interface SerializedFilterQuery {
+ kuery: KueryFilterQuery | null;
+ serializedQuery: string;
+}
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 8974f0b5b4d58..81dc44f3a4cb4 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -8225,11 +8225,8 @@
"xpack.ingestManager.agentConfigList.addButton": "エージェント構成を作成",
"xpack.ingestManager.agentConfigList.agentsColumnTitle": "エージェント",
"xpack.ingestManager.agentConfigList.clearFiltersLinkText": "フィルターを消去",
- "xpack.ingestManager.agentConfigList.copyConfigActionText": "構成をコピー",
"xpack.ingestManager.agentConfigList.createDatasourceActionText": "データソースを作成",
"xpack.ingestManager.agentConfigList.datasourcesCountColumnTitle": "データソース",
- "xpack.ingestManager.agentConfigList.deleteButton": "{count, plural, one {# エージェント設定} other {# エージェント設定}}を削除",
- "xpack.ingestManager.agentConfigList.deleteConfigActionText": "構成の削除",
"xpack.ingestManager.agentConfigList.descriptionColumnTitle": "説明",
"xpack.ingestManager.agentConfigList.loadingAgentConfigsMessage": "エージェント構成を読み込み中...",
"xpack.ingestManager.agentConfigList.nameColumnTitle": "名前",
@@ -8313,7 +8310,6 @@
"xpack.ingestManager.configDetails.configDetailsTitle": "構成「{id}」",
"xpack.ingestManager.configDetails.configNotFoundErrorTitle": "構成「{id}」が見つかりません",
"xpack.ingestManager.configDetails.datasourcesTable.actionsColumnTitle": "アクション",
- "xpack.ingestManager.configDetails.datasourcesTable.copyActionTitle": "データソースをコピー",
"xpack.ingestManager.configDetails.datasourcesTable.deleteActionTitle": "データソースを削除",
"xpack.ingestManager.configDetails.datasourcesTable.descriptionColumnTitle": "説明",
"xpack.ingestManager.configDetails.datasourcesTable.editActionTitle": "データソースを編集",
@@ -8321,7 +8317,6 @@
"xpack.ingestManager.configDetails.datasourcesTable.namespaceColumnTitle": "名前空間",
"xpack.ingestManager.configDetails.datasourcesTable.packageNameColumnTitle": "パッケージ",
"xpack.ingestManager.configDetails.datasourcesTable.streamsCountColumnTitle": "ストリーム",
- "xpack.ingestManager.configDetails.datasourcesTable.viewActionTitle": "データソースを表示",
"xpack.ingestManager.configDetails.subTabs.datasouces": "データソース",
"xpack.ingestManager.configDetails.subTabs.settings": "設定",
"xpack.ingestManager.configDetails.subTabs.yamlFile": "YAML ファイル",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index d36a62f15aee9..e06edb45de8fa 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -8228,11 +8228,8 @@
"xpack.ingestManager.agentConfigList.addButton": "创建代理配置",
"xpack.ingestManager.agentConfigList.agentsColumnTitle": "代理",
"xpack.ingestManager.agentConfigList.clearFiltersLinkText": "清除筛选",
- "xpack.ingestManager.agentConfigList.copyConfigActionText": "复制配置",
"xpack.ingestManager.agentConfigList.createDatasourceActionText": "创建数据源",
"xpack.ingestManager.agentConfigList.datasourcesCountColumnTitle": "数据源",
- "xpack.ingestManager.agentConfigList.deleteButton": "删除 {count, plural, one {# 个代理配置} other {# 个代理配置}}",
- "xpack.ingestManager.agentConfigList.deleteConfigActionText": "删除配置",
"xpack.ingestManager.agentConfigList.descriptionColumnTitle": "描述",
"xpack.ingestManager.agentConfigList.loadingAgentConfigsMessage": "正在加载代理配置……",
"xpack.ingestManager.agentConfigList.nameColumnTitle": "名称",
@@ -8316,7 +8313,6 @@
"xpack.ingestManager.configDetails.configDetailsTitle": "配置“{id}”",
"xpack.ingestManager.configDetails.configNotFoundErrorTitle": "未找到配置“{id}”",
"xpack.ingestManager.configDetails.datasourcesTable.actionsColumnTitle": "操作",
- "xpack.ingestManager.configDetails.datasourcesTable.copyActionTitle": "复制数据源",
"xpack.ingestManager.configDetails.datasourcesTable.deleteActionTitle": "删除数据源",
"xpack.ingestManager.configDetails.datasourcesTable.descriptionColumnTitle": "描述",
"xpack.ingestManager.configDetails.datasourcesTable.editActionTitle": "编辑数据源",
@@ -8324,7 +8320,6 @@
"xpack.ingestManager.configDetails.datasourcesTable.namespaceColumnTitle": "命名空间",
"xpack.ingestManager.configDetails.datasourcesTable.packageNameColumnTitle": "软件包",
"xpack.ingestManager.configDetails.datasourcesTable.streamsCountColumnTitle": "流计数",
- "xpack.ingestManager.configDetails.datasourcesTable.viewActionTitle": "查看数据源",
"xpack.ingestManager.configDetails.subTabs.datasouces": "数据源",
"xpack.ingestManager.configDetails.subTabs.settings": "设置",
"xpack.ingestManager.configDetails.subTabs.yamlFile": "YAML 文件",
diff --git a/x-pack/test/api_integration/apis/infra/metrics_alerting.ts b/x-pack/test/api_integration/apis/infra/metrics_alerting.ts
index 4f17f9db67483..19879f5761ab2 100644
--- a/x-pack/test/api_integration/apis/infra/metrics_alerting.ts
+++ b/x-pack/test/api_integration/apis/infra/metrics_alerting.ts
@@ -39,6 +39,7 @@ export default function({ getService }: FtrProviderContext) {
});
expect(result.error).to.not.be.ok();
expect(result.hits).to.be.ok();
+ expect(result.aggregations).to.be.ok();
});
}
it('should work with a filterQuery', async () => {
@@ -53,6 +54,21 @@ export default function({ getService }: FtrProviderContext) {
});
expect(result.error).to.not.be.ok();
expect(result.hits).to.be.ok();
+ expect(result.aggregations).to.be.ok();
+ });
+ it('should work with a filterQuery in KQL format', async () => {
+ const searchBody = getElasticsearchMetricQuery(
+ getSearchParams('avg'),
+ undefined,
+ '"agent.hostname":"foo"'
+ );
+ const result = await client.search({
+ index,
+ body: searchBody,
+ });
+ expect(result.error).to.not.be.ok();
+ expect(result.hits).to.be.ok();
+ expect(result.aggregations).to.be.ok();
});
});
describe('querying with a groupBy parameter', () => {
@@ -65,6 +81,7 @@ export default function({ getService }: FtrProviderContext) {
});
expect(result.error).to.not.be.ok();
expect(result.hits).to.be.ok();
+ expect(result.aggregations).to.be.ok();
});
}
it('should work with a filterQuery', async () => {
@@ -79,6 +96,7 @@ export default function({ getService }: FtrProviderContext) {
});
expect(result.error).to.not.be.ok();
expect(result.hits).to.be.ok();
+ expect(result.aggregations).to.be.ok();
});
});
});
diff --git a/x-pack/test/functional/apps/dashboard/reporting/index.js b/x-pack/test/functional/apps/dashboard/reporting/index.ts
similarity index 94%
rename from x-pack/test/functional/apps/dashboard/reporting/index.js
rename to x-pack/test/functional/apps/dashboard/reporting/index.ts
index 99be084d80d74..796e15b4e270f 100644
--- a/x-pack/test/functional/apps/dashboard/reporting/index.js
+++ b/x-pack/test/functional/apps/dashboard/reporting/index.ts
@@ -5,9 +5,10 @@
*/
import expect from '@kbn/expect';
-import path from 'path';
import fs from 'fs';
+import path from 'path';
import { promisify } from 'util';
+import { FtrProviderContext } from '../../../ftr_provider_context';
import { checkIfPngsMatch } from './lib/compare_pngs';
const writeFileAsync = promisify(fs.writeFile);
@@ -15,7 +16,7 @@ const mkdirAsync = promisify(fs.mkdir);
const REPORTS_FOLDER = path.resolve(__dirname, 'reports');
-export default function({ getService, getPageObjects }) {
+export default function({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const browser = getService('browser');
const log = getService('log');
@@ -85,14 +86,14 @@ export default function({ getService, getPageObjects }) {
describe('Preserve Layout', () => {
it('matches baseline report', async function() {
- const writeSessionReport = async (name, rawPdf, reportExt) => {
+ const writeSessionReport = async (name: string, rawPdf: Buffer, reportExt: string) => {
const sessionDirectory = path.resolve(REPORTS_FOLDER, 'session');
await mkdirAsync(sessionDirectory, { recursive: true });
const sessionReportPath = path.resolve(sessionDirectory, `${name}.${reportExt}`);
await writeFileAsync(sessionReportPath, rawPdf);
return sessionReportPath;
};
- const getBaselineReportPath = (fileName, reportExt) => {
+ const getBaselineReportPath = (fileName: string, reportExt: string) => {
const baselineFolder = path.resolve(REPORTS_FOLDER, 'baseline');
const fullPath = path.resolve(baselineFolder, `${fileName}.${reportExt}`);
log.debug(`getBaselineReportPath (${fullPath})`);
diff --git a/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.js b/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts
similarity index 90%
rename from x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.js
rename to x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts
index 13c97a7fce785..b2eb645c8372c 100644
--- a/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.js
+++ b/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts
@@ -4,14 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import path from 'path';
-import fs from 'fs';
import { promisify } from 'bluebird';
+import fs from 'fs';
+import path from 'path';
import { comparePngs } from '../../../../../../../test/functional/services/lib/compare_pngs';
-const mkdirAsync = promisify(fs.mkdir);
+const mkdirAsync = promisify(fs.mkdir);
-export async function checkIfPngsMatch(actualpngPath, baselinepngPath, screenshotsDirectory, log) {
+export async function checkIfPngsMatch(
+ actualpngPath: string,
+ baselinepngPath: string,
+ screenshotsDirectory: string,
+ log: any
+) {
log.debug(`checkIfpngsMatch: ${actualpngPath} vs ${baselinepngPath}`);
// Copy the pngs into the screenshot session directory, as that's where the generated pngs will automatically be
// stored.
diff --git a/x-pack/test/functional/apps/discover/reporting.js b/x-pack/test/functional/apps/discover/reporting.ts
similarity index 96%
rename from x-pack/test/functional/apps/discover/reporting.js
rename to x-pack/test/functional/apps/discover/reporting.ts
index 4aa005fc2db55..7a33e7f5135d4 100644
--- a/x-pack/test/functional/apps/discover/reporting.js
+++ b/x-pack/test/functional/apps/discover/reporting.ts
@@ -5,8 +5,9 @@
*/
import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
-export default function({ getService, getPageObjects }) {
+export default function({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');
const esArchiver = getService('esArchiver');
const browser = getService('browser');
diff --git a/x-pack/test/functional/apps/visualize/reporting.js b/x-pack/test/functional/apps/visualize/reporting.ts
similarity index 94%
rename from x-pack/test/functional/apps/visualize/reporting.js
rename to x-pack/test/functional/apps/visualize/reporting.ts
index bc252e1ad4134..5ef954e334d81 100644
--- a/x-pack/test/functional/apps/visualize/reporting.js
+++ b/x-pack/test/functional/apps/visualize/reporting.ts
@@ -5,8 +5,9 @@
*/
import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
-export default function({ getService, getPageObjects }) {
+export default function({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const browser = getService('browser');
const log = getService('log');
diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts
index 143a0b956985c..833cc452a5d31 100644
--- a/x-pack/test/functional/page_objects/index.ts
+++ b/x-pack/test/functional/page_objects/index.ts
@@ -19,7 +19,6 @@ import { GraphPageProvider } from './graph_page';
import { GrokDebuggerPageProvider } from './grok_debugger_page';
// @ts-ignore not ts yet
import { WatcherPageProvider } from './watcher_page';
-// @ts-ignore not ts yet
import { ReportingPageProvider } from './reporting_page';
// @ts-ignore not ts yet
import { AccountSettingProvider } from './accountsetting_page';
diff --git a/x-pack/test/functional/page_objects/reporting_page.js b/x-pack/test/functional/page_objects/reporting_page.ts
similarity index 84%
rename from x-pack/test/functional/page_objects/reporting_page.js
rename to x-pack/test/functional/page_objects/reporting_page.ts
index b24ba8cf95d1c..2c20519a8d214 100644
--- a/x-pack/test/functional/page_objects/reporting_page.js
+++ b/x-pack/test/functional/page_objects/reporting_page.ts
@@ -4,25 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import http, { IncomingMessage } from 'http';
+import { FtrProviderContext } from 'test/functional/ftr_provider_context';
import { parse } from 'url';
-import http from 'http';
-/*
- * NOTE: Reporting is a service, not an app. The page objects that are
- * important for generating reports belong to the apps that integrate with the
- * Reporting service. Eventually, this file should be dissolved across the
- * apps that need it for testing their integration.
- * Issue: https://github.com/elastic/kibana/issues/52927
- */
-export function ReportingPageProvider({ getService, getPageObjects }) {
+export function ReportingPageProvider({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const log = getService('log');
const testSubjects = getService('testSubjects');
const browser = getService('browser');
- const PageObjects = getPageObjects(['common', 'security', 'share', 'timePicker']);
+ const PageObjects = getPageObjects(['common', 'security' as any, 'share', 'timePicker']); // FIXME: Security PageObject is not Typescript
class ReportingPage {
- async forceSharedItemsContainerSize({ width }) {
+ async forceSharedItemsContainerSize({ width }: { width: number }) {
await browser.execute(`
var el = document.querySelector('[data-shared-items-container]');
el.style.flex="none";
@@ -30,7 +24,7 @@ export function ReportingPageProvider({ getService, getPageObjects }) {
`);
}
- async getReportURL(timeout) {
+ async getReportURL(timeout: number) {
log.debug('getReportURL');
const url = await testSubjects.getAttribute('downloadCompletedReportButton', 'href', timeout);
@@ -48,7 +42,7 @@ export function ReportingPageProvider({ getService, getPageObjects }) {
`);
}
- getResponse(url) {
+ getResponse(url: string): Promise {
log.debug(`getResponse for ${url}`);
const auth = 'test_user:changeme'; // FIXME not sure why there is no config that can be read for this
const headers = {
@@ -62,29 +56,30 @@ export function ReportingPageProvider({ getService, getPageObjects }) {
hostname: parsedUrl.hostname,
path: parsedUrl.path,
port: parsedUrl.port,
- responseType: 'arraybuffer',
headers,
},
- res => {
+ (res: IncomingMessage) => {
resolve(res);
}
)
- .on('error', e => {
+ .on('error', (e: Error) => {
log.error(e);
reject(e);
});
});
}
- async getRawPdfReportData(url) {
- const data = []; // List of Buffer objects
+ async getRawPdfReportData(url: string): Promise {
+ const data: Buffer[] = []; // List of Buffer objects
log.debug(`getRawPdfReportData for ${url}`);
return new Promise(async (resolve, reject) => {
const response = await this.getResponse(url).catch(reject);
- response.on('data', chunk => data.push(chunk));
- response.on('end', () => resolve(Buffer.concat(data)));
+ if (response) {
+ response.on('data', (chunk: Buffer) => data.push(chunk));
+ response.on('end', () => resolve(Buffer.concat(data)));
+ }
});
}
diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts
index e5b840b335846..d7bbc29bd861e 100644
--- a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts
+++ b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts
@@ -19,7 +19,9 @@ export default function({ getService }: FtrProviderContext) {
const log = getService('log');
const retry = getService('retry');
- describe('Event Log public API', () => {
+ // FLAKY: https://github.com/elastic/kibana/issues/64723
+ // FLAKY: https://github.com/elastic/kibana/issues/64812
+ describe.skip('Event Log public API', () => {
it('should allow querying for events by Saved Object', async () => {
const id = uuid.v4();