From a906c732b02cb54e9f19c00c01c2a526ce2d450f Mon Sep 17 00:00:00 2001
From: Nicolas Chaulet
Date: Mon, 13 Jul 2020 11:00:58 -0400
Subject: [PATCH 01/40] [Ingest Manager] During fleet setup create an
enrollment for every config (#71308)
---
.../ingest_manager/server/services/setup.ts | 15 +++++++++++----
1 file changed, 11 insertions(+), 4 deletions(-)
diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts
index e27a5456a5a7d..627abc158143d 100644
--- a/x-pack/plugins/ingest_manager/server/services/setup.ts
+++ b/x-pack/plugins/ingest_manager/server/services/setup.ts
@@ -180,11 +180,18 @@ export async function setupFleet(
fleet_enroll_password: password,
});
- // Generate default enrollment key
- await generateEnrollmentAPIKey(soClient, {
- name: 'Default',
- configId: await agentConfigService.getDefaultAgentConfigId(soClient),
+ const { items: agentConfigs } = await agentConfigService.list(soClient, {
+ perPage: 10000,
});
+
+ await Promise.all(
+ agentConfigs.map((agentConfig) => {
+ return generateEnrollmentAPIKey(soClient, {
+ name: `Default`,
+ configId: agentConfig.id,
+ });
+ })
+ );
}
function generateRandomPassword() {
From f0d744e8659a0f9d8369226084151bff235afe2c Mon Sep 17 00:00:00 2001
From: Jean-Louis Leysens
Date: Mon, 13 Jul 2020 17:03:41 +0200
Subject: [PATCH 02/40] inclusive language (#71438)
---
.../repository_form/type_settings/readonly_settings.tsx | 2 +-
x-pack/plugins/translations/translations/ja-JP.json | 1 -
x-pack/plugins/translations/translations/zh-CN.json | 1 -
3 files changed, 1 insertion(+), 3 deletions(-)
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/repository_form/type_settings/readonly_settings.tsx b/x-pack/plugins/snapshot_restore/public/application/components/repository_form/type_settings/readonly_settings.tsx
index 309dad366bef8..17cce6efafb6f 100644
--- a/x-pack/plugins/snapshot_restore/public/application/components/repository_form/type_settings/readonly_settings.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/components/repository_form/type_settings/readonly_settings.tsx
@@ -46,7 +46,7 @@ export const ReadonlySettings: React.FunctionComponent = ({
case 'ftp':
return (
repositories.url.allowed_urls,
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 92285d8bf72f8..4118053396e90 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -15258,7 +15258,6 @@
"xpack.snapshotRestore.repositoryForm.typeReadonly.urlLabel": "パス (必須)",
"xpack.snapshotRestore.repositoryForm.typeReadonly.urlSchemeLabel": "スキーム",
"xpack.snapshotRestore.repositoryForm.typeReadonly.urlTitle": "URL",
- "xpack.snapshotRestore.repositoryForm.typeReadonly.urlWhitelistDescription": "この URL は {settingKey} 設定で登録する必要があります。",
"xpack.snapshotRestore.repositoryForm.typeS3.basePathDescription": "レポジトリデータへのバケットパスです。",
"xpack.snapshotRestore.repositoryForm.typeS3.basePathLabel": "ベースパス",
"xpack.snapshotRestore.repositoryForm.typeS3.basePathTitle": "ベースパス",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 457f65e89083d..01939bea417d4 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -15264,7 +15264,6 @@
"xpack.snapshotRestore.repositoryForm.typeReadonly.urlLabel": "路径(必填)",
"xpack.snapshotRestore.repositoryForm.typeReadonly.urlSchemeLabel": "方案",
"xpack.snapshotRestore.repositoryForm.typeReadonly.urlTitle": "URL",
- "xpack.snapshotRestore.repositoryForm.typeReadonly.urlWhitelistDescription": "必须在 {settingKey} 设置中注册此 URL。",
"xpack.snapshotRestore.repositoryForm.typeS3.basePathDescription": "存储库数据的存储桶路径。",
"xpack.snapshotRestore.repositoryForm.typeS3.basePathLabel": "基路径",
"xpack.snapshotRestore.repositoryForm.typeS3.basePathTitle": "基路径",
From f0c9915280f5c92e1e32cdc39a70b84ef4cea367 Mon Sep 17 00:00:00 2001
From: Oliver Gupte
Date: Mon, 13 Jul 2020 08:09:04 -0700
Subject: [PATCH 03/40] [APM] Anomaly detection integration with transaction
duration chart (#71230)
* Reintroduces the previous anomaly detection ML integration back into the
transaction duration chart in the service details screen. Support the
latest APM anoamly detection by environment jobs.
* PR feedback
* Code improvements from PR feedback
* handle errors thrown when fetching ml job for current environment
Co-authored-by: Elastic Machine
---
.../app/TransactionDetails/index.tsx | 1 -
.../app/TransactionOverview/index.tsx | 2 -
.../shared/charts/TransactionCharts/index.tsx | 18 ++-
.../apm/public/selectors/chartSelectors.ts | 2 +
.../charts/get_anomaly_data/fetcher.ts | 93 ++++++++++++
.../get_anomaly_data/get_ml_bucket_size.ts | 61 ++++++++
.../charts/get_anomaly_data/index.ts | 72 ++++++++--
.../charts/get_anomaly_data/transform.ts | 136 ++++++++++++++++++
.../server/lib/transactions/charts/index.ts | 4 +
.../server/lib/transactions/queries.test.ts | 8 ++
.../apm/server/routes/transaction_groups.ts | 16 ++-
11 files changed, 390 insertions(+), 23 deletions(-)
create mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts
create mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/get_ml_bucket_size.ts
create mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.ts
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx
index 620ae6708eda0..c56b7b9aaa720 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx
@@ -89,7 +89,6 @@ export function TransactionDetails() {
{
};
public renderMLHeader(hasValidMlLicense: boolean | undefined) {
- const { hasMLJob } = this.props;
- if (!hasValidMlLicense || !hasMLJob) {
+ const { mlJobId } = this.props.charts;
+
+ if (!hasValidMlLicense || !mlJobId) {
return null;
}
- const { serviceName, kuery } = this.props.urlParams;
+ const { serviceName, kuery, transactionType } = this.props.urlParams;
if (!serviceName) {
return null;
}
- const linkedJobId = ''; // TODO [APM ML] link to ML job id for the selected environment
-
const hasKuery = !isEmpty(kuery);
const icon = hasKuery ? (
{
}
)}{' '}
- View Job
+
+ View Job
+
);
diff --git a/x-pack/plugins/apm/public/selectors/chartSelectors.ts b/x-pack/plugins/apm/public/selectors/chartSelectors.ts
index 714d62a703f51..26c2365ed77e1 100644
--- a/x-pack/plugins/apm/public/selectors/chartSelectors.ts
+++ b/x-pack/plugins/apm/public/selectors/chartSelectors.ts
@@ -33,6 +33,7 @@ export interface ITpmBucket {
export interface ITransactionChartData {
tpmSeries: ITpmBucket[];
responseTimeSeries: TimeSeries[];
+ mlJobId: string | undefined;
}
const INITIAL_DATA = {
@@ -62,6 +63,7 @@ export function getTransactionCharts(
return {
tpmSeries,
responseTimeSeries,
+ mlJobId: anomalyTimeseries?.jobId,
};
}
diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts
new file mode 100644
index 0000000000000..3cf9a54e3fe9b
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts
@@ -0,0 +1,93 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Logger } from 'kibana/server';
+import { PromiseReturnType } from '../../../../../../observability/typings/common';
+import { Setup, SetupTimeRange } from '../../../helpers/setup_request';
+
+export type ESResponse = Exclude<
+ PromiseReturnType,
+ undefined
+>;
+
+export async function anomalySeriesFetcher({
+ serviceName,
+ transactionType,
+ intervalString,
+ mlBucketSize,
+ setup,
+ jobId,
+ logger,
+}: {
+ serviceName: string;
+ transactionType: string;
+ intervalString: string;
+ mlBucketSize: number;
+ setup: Setup & SetupTimeRange;
+ jobId: string;
+ logger: Logger;
+}) {
+ const { ml, start, end } = setup;
+ if (!ml) {
+ return;
+ }
+
+ // move the start back with one bucket size, to ensure to get anomaly data in the beginning
+ // this is required because ML has a minimum bucket size (default is 900s) so if our buckets are smaller, we might have several null buckets in the beginning
+ const newStart = start - mlBucketSize * 1000;
+
+ const params = {
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [
+ { term: { job_id: jobId } },
+ { exists: { field: 'bucket_span' } },
+ { term: { result_type: 'model_plot' } },
+ { term: { partition_field_value: serviceName } },
+ { term: { by_field_value: transactionType } },
+ {
+ range: {
+ timestamp: { gte: newStart, lte: end, format: 'epoch_millis' },
+ },
+ },
+ ],
+ },
+ },
+ aggs: {
+ ml_avg_response_times: {
+ date_histogram: {
+ field: 'timestamp',
+ fixed_interval: intervalString,
+ min_doc_count: 0,
+ extended_bounds: { min: newStart, max: end },
+ },
+ aggs: {
+ anomaly_score: { max: { field: 'anomaly_score' } },
+ lower: { min: { field: 'model_lower' } },
+ upper: { max: { field: 'model_upper' } },
+ },
+ },
+ },
+ },
+ };
+
+ try {
+ const response = await ml.mlSystem.mlAnomalySearch(params);
+ return response;
+ } catch (err) {
+ const isHttpError = 'statusCode' in err;
+ if (isHttpError) {
+ logger.info(
+ `Status code "${err.statusCode}" while retrieving ML anomalies for APM`
+ );
+ return;
+ }
+ logger.error('An error occurred while retrieving ML anomalies for APM');
+ logger.error(err);
+ }
+}
diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/get_ml_bucket_size.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/get_ml_bucket_size.ts
new file mode 100644
index 0000000000000..2f5e703251c03
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/get_ml_bucket_size.ts
@@ -0,0 +1,61 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Logger } from 'kibana/server';
+import { Setup, SetupTimeRange } from '../../../helpers/setup_request';
+
+interface IOptions {
+ setup: Setup & SetupTimeRange;
+ jobId: string;
+ logger: Logger;
+}
+
+interface ESResponse {
+ bucket_span: number;
+}
+
+export async function getMlBucketSize({
+ setup,
+ jobId,
+ logger,
+}: IOptions): Promise {
+ const { ml, start, end } = setup;
+ if (!ml) {
+ return;
+ }
+
+ const params = {
+ body: {
+ _source: 'bucket_span',
+ size: 1,
+ terminateAfter: 1,
+ query: {
+ bool: {
+ filter: [
+ { term: { job_id: jobId } },
+ { exists: { field: 'bucket_span' } },
+ {
+ range: {
+ timestamp: { gte: start, lte: end, format: 'epoch_millis' },
+ },
+ },
+ ],
+ },
+ },
+ },
+ };
+
+ try {
+ const resp = await ml.mlSystem.mlAnomalySearch(params);
+ return resp.hits.hits[0]?._source.bucket_span;
+ } catch (err) {
+ const isHttpError = 'statusCode' in err;
+ if (isHttpError) {
+ return;
+ }
+ logger.error(err);
+ }
+}
diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts
index b2d11f2ffe19a..072099bc9553c 100644
--- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts
+++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts
@@ -3,18 +3,19 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-
+import { Logger } from 'kibana/server';
+import { isNumber } from 'lodash';
+import { getBucketSize } from '../../../helpers/get_bucket_size';
import {
Setup,
SetupTimeRange,
SetupUIFilters,
} from '../../../helpers/setup_request';
-import { Coordinate, RectCoordinate } from '../../../../../typings/timeseries';
-
-interface AnomalyTimeseries {
- anomalyBoundaries: Coordinate[];
- anomalyScore: RectCoordinate[];
-}
+import { anomalySeriesFetcher } from './fetcher';
+import { getMlBucketSize } from './get_ml_bucket_size';
+import { anomalySeriesTransform } from './transform';
+import { getMLJobIds } from '../../../service_map/get_service_anomalies';
+import { UIFilters } from '../../../../../typings/ui_filters';
export async function getAnomalySeries({
serviceName,
@@ -22,13 +23,17 @@ export async function getAnomalySeries({
transactionName,
timeSeriesDates,
setup,
+ logger,
+ uiFilters,
}: {
serviceName: string;
transactionType: string | undefined;
transactionName: string | undefined;
timeSeriesDates: number[];
setup: Setup & SetupTimeRange & SetupUIFilters;
-}): Promise {
+ logger: Logger;
+ uiFilters: UIFilters;
+}) {
// don't fetch anomalies for transaction details page
if (transactionName) {
return;
@@ -39,8 +44,12 @@ export async function getAnomalySeries({
return;
}
- // don't fetch anomalies if uiFilters are applied
- if (setup.uiFiltersES.length > 0) {
+ // don't fetch anomalies if unknown uiFilters are applied
+ const knownFilters = ['environment', 'serviceName'];
+ const uiFilterNames = Object.keys(uiFilters);
+ if (
+ uiFilterNames.some((uiFilterName) => !knownFilters.includes(uiFilterName))
+ ) {
return;
}
@@ -55,6 +64,45 @@ export async function getAnomalySeries({
return;
}
- // TODO [APM ML] return a series of anomaly scores, upper & lower bounds for the given timeSeriesDates
- return;
+ let mlJobIds: string[] = [];
+ try {
+ mlJobIds = await getMLJobIds(setup.ml, uiFilters.environment);
+ } catch (error) {
+ logger.error(error);
+ return;
+ }
+
+ // don't fetch anomalies if there are isn't exaclty 1 ML job match for the given environment
+ if (mlJobIds.length !== 1) {
+ return;
+ }
+ const jobId = mlJobIds[0];
+
+ const mlBucketSize = await getMlBucketSize({ setup, jobId, logger });
+ if (!isNumber(mlBucketSize)) {
+ return;
+ }
+
+ const { start, end } = setup;
+ const { intervalString, bucketSize } = getBucketSize(start, end, 'auto');
+
+ const esResponse = await anomalySeriesFetcher({
+ serviceName,
+ transactionType,
+ intervalString,
+ mlBucketSize,
+ setup,
+ jobId,
+ logger,
+ });
+
+ if (esResponse && mlBucketSize > 0) {
+ return anomalySeriesTransform(
+ esResponse,
+ mlBucketSize,
+ bucketSize,
+ timeSeriesDates,
+ jobId
+ );
+ }
}
diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.ts
new file mode 100644
index 0000000000000..393a73f7c1ccd
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.ts
@@ -0,0 +1,136 @@
+/*
+ * 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 { first, last } from 'lodash';
+import { Coordinate, RectCoordinate } from '../../../../../typings/timeseries';
+import { ESResponse } from './fetcher';
+
+type IBucket = ReturnType;
+function getBucket(
+ bucket: Required<
+ ESResponse
+ >['aggregations']['ml_avg_response_times']['buckets'][0]
+) {
+ return {
+ x: bucket.key,
+ anomalyScore: bucket.anomaly_score.value,
+ lower: bucket.lower.value,
+ upper: bucket.upper.value,
+ };
+}
+
+export type AnomalyTimeSeriesResponse = ReturnType<
+ typeof anomalySeriesTransform
+>;
+export function anomalySeriesTransform(
+ response: ESResponse,
+ mlBucketSize: number,
+ bucketSize: number,
+ timeSeriesDates: number[],
+ jobId: string
+) {
+ const buckets =
+ response.aggregations?.ml_avg_response_times.buckets.map(getBucket) || [];
+
+ const bucketSizeInMillis = Math.max(bucketSize, mlBucketSize) * 1000;
+
+ return {
+ jobId,
+ anomalyScore: getAnomalyScoreDataPoints(
+ buckets,
+ timeSeriesDates,
+ bucketSizeInMillis
+ ),
+ anomalyBoundaries: getAnomalyBoundaryDataPoints(buckets, timeSeriesDates),
+ };
+}
+
+export function getAnomalyScoreDataPoints(
+ buckets: IBucket[],
+ timeSeriesDates: number[],
+ bucketSizeInMillis: number
+): RectCoordinate[] {
+ const ANOMALY_THRESHOLD = 75;
+ const firstDate = first(timeSeriesDates);
+ const lastDate = last(timeSeriesDates);
+
+ if (firstDate === undefined || lastDate === undefined) {
+ return [];
+ }
+
+ return buckets
+ .filter(
+ (bucket) =>
+ bucket.anomalyScore !== null && bucket.anomalyScore > ANOMALY_THRESHOLD
+ )
+ .filter(isInDateRange(firstDate, lastDate))
+ .map((bucket) => {
+ return {
+ x0: bucket.x,
+ x: Math.min(bucket.x + bucketSizeInMillis, lastDate), // don't go beyond last date
+ };
+ });
+}
+
+export function getAnomalyBoundaryDataPoints(
+ buckets: IBucket[],
+ timeSeriesDates: number[]
+): Coordinate[] {
+ return replaceFirstAndLastBucket(buckets, timeSeriesDates)
+ .filter((bucket) => bucket.lower !== null)
+ .map((bucket) => {
+ return {
+ x: bucket.x,
+ y0: bucket.lower,
+ y: bucket.upper,
+ };
+ });
+}
+
+export function replaceFirstAndLastBucket(
+ buckets: IBucket[],
+ timeSeriesDates: number[]
+) {
+ const firstDate = first(timeSeriesDates);
+ const lastDate = last(timeSeriesDates);
+
+ if (firstDate === undefined || lastDate === undefined) {
+ return buckets;
+ }
+
+ const preBucketWithValue = buckets
+ .filter((p) => p.x <= firstDate)
+ .reverse()
+ .find((p) => p.lower !== null);
+
+ const bucketsInRange = buckets.filter(isInDateRange(firstDate, lastDate));
+
+ // replace first bucket if it is null
+ const firstBucket = first(bucketsInRange);
+ if (preBucketWithValue && firstBucket && firstBucket.lower === null) {
+ firstBucket.lower = preBucketWithValue.lower;
+ firstBucket.upper = preBucketWithValue.upper;
+ }
+
+ const lastBucketWithValue = [...buckets]
+ .reverse()
+ .find((p) => p.lower !== null);
+
+ // replace last bucket if it is null
+ const lastBucket = last(bucketsInRange);
+ if (lastBucketWithValue && lastBucket && lastBucket.lower === null) {
+ lastBucket.lower = lastBucketWithValue.lower;
+ lastBucket.upper = lastBucketWithValue.upper;
+ }
+
+ return bucketsInRange;
+}
+
+// anomaly time series contain one or more buckets extra in the beginning
+// these extra buckets should be removed
+function isInDateRange(firstDate: number, lastDate: number) {
+ return (p: IBucket) => p.x >= firstDate && p.x <= lastDate;
+}
diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/index.ts b/x-pack/plugins/apm/server/lib/transactions/charts/index.ts
index 2ec049002d605..e862982145f77 100644
--- a/x-pack/plugins/apm/server/lib/transactions/charts/index.ts
+++ b/x-pack/plugins/apm/server/lib/transactions/charts/index.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { Logger } from 'kibana/server';
import { PromiseReturnType } from '../../../../../observability/typings/common';
import {
Setup,
@@ -13,6 +14,7 @@ import {
import { getAnomalySeries } from './get_anomaly_data';
import { getApmTimeseriesData } from './get_timeseries_data';
import { ApmTimeSeriesResponse } from './get_timeseries_data/transform';
+import { UIFilters } from '../../../../typings/ui_filters';
function getDates(apmTimeseries: ApmTimeSeriesResponse) {
return apmTimeseries.responseTimes.avg.map((p) => p.x);
@@ -26,6 +28,8 @@ export async function getTransactionCharts(options: {
transactionType: string | undefined;
transactionName: string | undefined;
setup: Setup & SetupTimeRange & SetupUIFilters;
+ logger: Logger;
+ uiFilters: UIFilters;
}) {
const apmTimeseries = await getApmTimeseriesData(options);
const anomalyTimeseries = await getAnomalySeries({
diff --git a/x-pack/plugins/apm/server/lib/transactions/queries.test.ts b/x-pack/plugins/apm/server/lib/transactions/queries.test.ts
index 713635cff2fbf..586fa1798b7bc 100644
--- a/x-pack/plugins/apm/server/lib/transactions/queries.test.ts
+++ b/x-pack/plugins/apm/server/lib/transactions/queries.test.ts
@@ -12,6 +12,8 @@ import {
SearchParamsMock,
inspectSearchParams,
} from '../../../public/utils/testHelpers';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { loggerMock } from '../../../../../../src/core/server/logging/logger.mock';
describe('transaction queries', () => {
let mock: SearchParamsMock;
@@ -52,6 +54,8 @@ describe('transaction queries', () => {
transactionName: undefined,
transactionType: undefined,
setup,
+ logger: loggerMock.create(),
+ uiFilters: {},
})
);
expect(mock.params).toMatchSnapshot();
@@ -64,6 +68,8 @@ describe('transaction queries', () => {
transactionName: 'bar',
transactionType: undefined,
setup,
+ logger: loggerMock.create(),
+ uiFilters: {},
})
);
expect(mock.params).toMatchSnapshot();
@@ -76,6 +82,8 @@ describe('transaction queries', () => {
transactionName: 'bar',
transactionType: 'baz',
setup,
+ logger: loggerMock.create(),
+ uiFilters: {},
})
);
diff --git a/x-pack/plugins/apm/server/routes/transaction_groups.ts b/x-pack/plugins/apm/server/routes/transaction_groups.ts
index 9ad281159fca5..3d939b04795c6 100644
--- a/x-pack/plugins/apm/server/routes/transaction_groups.ts
+++ b/x-pack/plugins/apm/server/routes/transaction_groups.ts
@@ -14,6 +14,7 @@ import { createRoute } from './create_route';
import { uiFiltersRt, rangeRt } from './default_api_types';
import { getTransactionAvgDurationByBrowser } from '../lib/transactions/avg_duration_by_browser';
import { getTransactionAvgDurationByCountry } from '../lib/transactions/avg_duration_by_country';
+import { UIFilters } from '../../typings/ui_filters';
export const transactionGroupsRoute = createRoute(() => ({
path: '/api/apm/services/{serviceName}/transaction_groups',
@@ -62,14 +63,27 @@ export const transactionGroupsChartsRoute = createRoute(() => ({
},
handler: async ({ context, request }) => {
const setup = await setupRequest(context, request);
+ const logger = context.logger;
const { serviceName } = context.params.path;
- const { transactionType, transactionName } = context.params.query;
+ const {
+ transactionType,
+ transactionName,
+ uiFilters: uiFiltersJson,
+ } = context.params.query;
+ let uiFilters: UIFilters = {};
+ try {
+ uiFilters = JSON.parse(uiFiltersJson);
+ } catch (error) {
+ logger.error(error);
+ }
return getTransactionCharts({
serviceName,
transactionType,
transactionName,
setup,
+ logger,
+ uiFilters,
});
},
}));
From ae231feef7c393d730e374bab11c46c592c848df Mon Sep 17 00:00:00 2001
From: Oliver Gupte
Date: Mon, 13 Jul 2020 08:09:55 -0700
Subject: [PATCH 04/40] [APM] Anomaly detection setup link with alert if job
doesn't exist (#71229)
* Closes #70440 by adding a setup link to anomaly detection setting in the home header
* PR feedback and type error fix
* Code cleanup and PR feedback
* Modified getEnvironmentUiFilterES return type from `ESFilter | undefined` to `ESFilter[]` for ease of use.
Co-authored-by: Elastic Machine
---
.../apm/common/environment_filter_values.ts | 11 +++
.../apm/public/components/app/Home/index.tsx | 4 +
.../anomaly_detection/add_environments.tsx | 11 +--
.../Settings/anomaly_detection/jobs_list.tsx | 11 +--
.../Links/apm/AnomalyDetectionSetupLink.tsx | 63 +++++++++++++
.../create_anomaly_detection_jobs.ts | 17 +---
.../get_environment_ui_filter_es.test.ts | 17 ++--
.../get_environment_ui_filter_es.ts | 15 +--
.../convert_ui_filters/get_ui_filters_es.ts | 13 +--
.../get_service_map_service_node_info.ts | 93 +++----------------
.../get_derived_service_annotations.ts | 7 +-
.../annotations/get_stored_annotations.ts | 4 +-
12 files changed, 117 insertions(+), 149 deletions(-)
create mode 100644 x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx
diff --git a/x-pack/plugins/apm/common/environment_filter_values.ts b/x-pack/plugins/apm/common/environment_filter_values.ts
index 239378d0ea94a..38b6f480ca3d3 100644
--- a/x-pack/plugins/apm/common/environment_filter_values.ts
+++ b/x-pack/plugins/apm/common/environment_filter_values.ts
@@ -4,5 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { i18n } from '@kbn/i18n';
+
export const ENVIRONMENT_ALL = 'ENVIRONMENT_ALL';
export const ENVIRONMENT_NOT_DEFINED = 'ENVIRONMENT_NOT_DEFINED';
+
+export function getEnvironmentLabel(environment: string) {
+ if (environment === ENVIRONMENT_NOT_DEFINED) {
+ return i18n.translate('xpack.apm.filter.environment.notDefinedLabel', {
+ defaultMessage: 'Not defined',
+ });
+ }
+ return environment;
+}
diff --git a/x-pack/plugins/apm/public/components/app/Home/index.tsx b/x-pack/plugins/apm/public/components/app/Home/index.tsx
index f612ac0d383ef..bcc834fef6a6a 100644
--- a/x-pack/plugins/apm/public/components/app/Home/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/Home/index.tsx
@@ -20,6 +20,7 @@ import { EuiTabLink } from '../../shared/EuiTabLink';
import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink';
import { ServiceOverviewLink } from '../../shared/Links/apm/ServiceOverviewLink';
import { SettingsLink } from '../../shared/Links/apm/SettingsLink';
+import { AnomalyDetectionSetupLink } from '../../shared/Links/apm/AnomalyDetectionSetupLink';
import { TraceOverviewLink } from '../../shared/Links/apm/TraceOverviewLink';
import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink';
import { ServiceMap } from '../ServiceMap';
@@ -118,6 +119,9 @@ export function Home({ tab }: Props) {
+
+
+
diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx
index 2da3c12563104..98b4ae2f4b63f 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx
@@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n';
import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher';
import { useApmPluginContext } from '../../../../hooks/useApmPluginContext';
import { createJobs } from './create_jobs';
-import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values';
+import { getEnvironmentLabel } from '../../../../../common/environment_filter_values';
interface Props {
currentEnvironments: string[];
@@ -45,7 +45,7 @@ export const AddEnvironments = ({
);
const environmentOptions = data.map((env) => ({
- label: env === ENVIRONMENT_NOT_DEFINED ? NOT_DEFINED_OPTION_LABEL : env,
+ label: getEnvironmentLabel(env),
value: env,
disabled: currentEnvironments.includes(env),
}));
@@ -155,10 +155,3 @@ export const AddEnvironments = ({
);
};
-
-const NOT_DEFINED_OPTION_LABEL = i18n.translate(
- 'xpack.apm.filter.environment.notDefinedLabel',
- {
- defaultMessage: 'Not defined',
- }
-);
diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx
index 83d19aa27ac11..34687e5a8094e 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx
@@ -22,7 +22,7 @@ import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
import { AnomalyDetectionJobByEnv } from '../../../../../typings/anomaly_detection';
import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink';
import { MLLink } from '../../../shared/Links/MachineLearningLinks/MLLink';
-import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values';
+import { getEnvironmentLabel } from '../../../../../common/environment_filter_values';
import { LegacyJobsCallout } from './legacy_jobs_callout';
const columns: Array> = [
@@ -32,14 +32,7 @@ const columns: Array> = [
'xpack.apm.settings.anomalyDetection.jobList.environmentColumnLabel',
{ defaultMessage: 'Environment' }
),
- render: (environment: string) => {
- if (environment === ENVIRONMENT_NOT_DEFINED) {
- return i18n.translate('xpack.apm.filter.environment.notDefinedLabel', {
- defaultMessage: 'Not defined',
- });
- }
- return environment;
- },
+ render: getEnvironmentLabel,
},
{
field: 'job_id',
diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx
new file mode 100644
index 0000000000000..88d15239b8fba
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx
@@ -0,0 +1,63 @@
+/*
+ * 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, EuiToolTip, EuiIcon } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { APMLink } from './APMLink';
+import { getEnvironmentLabel } from '../../../../../common/environment_filter_values';
+import { useUrlParams } from '../../../../hooks/useUrlParams';
+import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher';
+
+export function AnomalyDetectionSetupLink() {
+ const { uiFilters } = useUrlParams();
+ const environment = uiFilters.environment;
+
+ const { data = { jobs: [], hasLegacyJobs: false }, status } = useFetcher(
+ (callApmApi) =>
+ callApmApi({ pathname: `/api/apm/settings/anomaly-detection` }),
+ [],
+ { preservePreviousData: false }
+ );
+ const isFetchSuccess = status === FETCH_STATUS.SUCCESS;
+
+ // Show alert if there are no jobs OR if no job matches the current environment
+ const showAlert =
+ isFetchSuccess && !data.jobs.some((job) => environment === job.environment);
+
+ return (
+
+
+ {ANOMALY_DETECTION_LINK_LABEL}
+
+ {showAlert && (
+
+
+
+ )}
+
+ );
+}
+
+function getTooltipText(environment?: string) {
+ if (!environment) {
+ return i18n.translate('xpack.apm.anomalyDetectionSetup.notEnabledText', {
+ defaultMessage: `Anomaly detection is not yet enabled. Click to continue setup.`,
+ });
+ }
+
+ return i18n.translate(
+ 'xpack.apm.anomalyDetectionSetup.notEnabledForEnvironmentText',
+ {
+ defaultMessage: `Anomaly detection is not yet enabled for the "{currentEnvironment}" environment. Click to continue setup.`,
+ values: { currentEnvironment: getEnvironmentLabel(environment) },
+ }
+ );
+}
+
+const ANOMALY_DETECTION_LINK_LABEL = i18n.translate(
+ 'xpack.apm.anomalyDetectionSetup.linkLabel',
+ { defaultMessage: `Anomaly detection` }
+);
diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts
index e723393a24013..c387c5152b1c5 100644
--- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts
+++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts
@@ -10,12 +10,11 @@ import { snakeCase } from 'lodash';
import { PromiseReturnType } from '../../../../observability/typings/common';
import { Setup } from '../helpers/setup_request';
import {
- SERVICE_ENVIRONMENT,
TRANSACTION_DURATION,
PROCESSOR_EVENT,
} from '../../../common/elasticsearch_fieldnames';
-import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values';
import { APM_ML_JOB_GROUP, ML_MODULE_ID_APM_TRANSACTION } from './constants';
+import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es';
export type CreateAnomalyDetectionJobsAPIResponse = PromiseReturnType<
typeof createAnomalyDetectionJobs
@@ -89,9 +88,7 @@ async function createAnomalyDetectionJob({
filter: [
{ term: { [PROCESSOR_EVENT]: 'transaction' } },
{ exists: { field: TRANSACTION_DURATION } },
- environment === ENVIRONMENT_NOT_DEFINED
- ? ENVIRONMENT_NOT_DEFINED_FILTER
- : { term: { [SERVICE_ENVIRONMENT]: environment } },
+ ...getEnvironmentUiFilterES(environment),
],
},
},
@@ -109,13 +106,3 @@ async function createAnomalyDetectionJob({
],
});
}
-
-const ENVIRONMENT_NOT_DEFINED_FILTER = {
- bool: {
- must_not: {
- exists: {
- field: SERVICE_ENVIRONMENT,
- },
- },
- },
-};
diff --git a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/__test__/get_environment_ui_filter_es.test.ts b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/__test__/get_environment_ui_filter_es.test.ts
index 0f0a11a868d6d..800f809727eb6 100644
--- a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/__test__/get_environment_ui_filter_es.test.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/__test__/get_environment_ui_filter_es.test.ts
@@ -7,24 +7,23 @@
import { getEnvironmentUiFilterES } from '../get_environment_ui_filter_es';
import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values';
import { SERVICE_ENVIRONMENT } from '../../../../../common/elasticsearch_fieldnames';
-import { ESFilter } from '../../../../../typings/elasticsearch';
describe('getEnvironmentUiFilterES', () => {
- it('should return undefined, when environment is undefined', () => {
+ it('should return empty array, when environment is undefined', () => {
const uiFilterES = getEnvironmentUiFilterES();
- expect(uiFilterES).toBeUndefined();
+ expect(uiFilterES).toHaveLength(0);
});
it('should create a filter for a service environment', () => {
- const uiFilterES = getEnvironmentUiFilterES('test') as ESFilter;
- expect(uiFilterES).toHaveProperty(['term', SERVICE_ENVIRONMENT], 'test');
+ const uiFilterES = getEnvironmentUiFilterES('test');
+ expect(uiFilterES).toHaveLength(1);
+ expect(uiFilterES[0]).toHaveProperty(['term', SERVICE_ENVIRONMENT], 'test');
});
it('should create a filter for missing service environments', () => {
- const uiFilterES = getEnvironmentUiFilterES(
- ENVIRONMENT_NOT_DEFINED
- ) as ESFilter;
- expect(uiFilterES).toHaveProperty(
+ const uiFilterES = getEnvironmentUiFilterES(ENVIRONMENT_NOT_DEFINED);
+ expect(uiFilterES).toHaveLength(1);
+ expect(uiFilterES[0]).toHaveProperty(
['bool', 'must_not', 'exists', 'field'],
SERVICE_ENVIRONMENT
);
diff --git a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts
index 63d222a7fcb6e..87bc8dc968373 100644
--- a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts
@@ -8,19 +8,12 @@ import { ESFilter } from '../../../../typings/elasticsearch';
import { ENVIRONMENT_NOT_DEFINED } from '../../../../common/environment_filter_values';
import { SERVICE_ENVIRONMENT } from '../../../../common/elasticsearch_fieldnames';
-export function getEnvironmentUiFilterES(
- environment?: string
-): ESFilter | undefined {
+export function getEnvironmentUiFilterES(environment?: string): ESFilter[] {
if (!environment) {
- return undefined;
+ return [];
}
-
if (environment === ENVIRONMENT_NOT_DEFINED) {
- return {
- bool: { must_not: { exists: { field: SERVICE_ENVIRONMENT } } },
- };
+ return [{ bool: { must_not: { exists: { field: SERVICE_ENVIRONMENT } } } }];
}
- return {
- term: { [SERVICE_ENVIRONMENT]: environment },
- };
+ return [{ term: { [SERVICE_ENVIRONMENT]: environment } }];
}
diff --git a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts
index b34d5535d58cc..c1405b44f2a8a 100644
--- a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts
@@ -27,22 +27,19 @@ export function getUiFiltersES(uiFilters: UIFilters) {
};
}) as ESFilter[];
- // remove undefined items from list
const esFilters = [
- getKueryUiFilterES(uiFilters.kuery),
- getEnvironmentUiFilterES(uiFilters.environment),
- ]
- .filter((filter) => !!filter)
- .concat(mappedFilters) as ESFilter[];
+ ...getKueryUiFilterES(uiFilters.kuery),
+ ...getEnvironmentUiFilterES(uiFilters.environment),
+ ].concat(mappedFilters) as ESFilter[];
return esFilters;
}
function getKueryUiFilterES(kuery?: string) {
if (!kuery) {
- return;
+ return [];
}
const ast = esKuery.fromKueryExpression(kuery);
- return esKuery.toElasticsearchQuery(ast) as ESFilter;
+ return [esKuery.toElasticsearchQuery(ast) as ESFilter];
}
diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts
index be92bfe5a0099..dd5d19b620c51 100644
--- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts
+++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts
@@ -9,7 +9,6 @@ import { ESFilter } from '../../../typings/elasticsearch';
import { rangeFilter } from '../../../common/utils/range_filter';
import {
PROCESSOR_EVENT,
- SERVICE_ENVIRONMENT,
SERVICE_NAME,
TRANSACTION_DURATION,
TRANSACTION_TYPE,
@@ -22,7 +21,7 @@ import {
TRANSACTION_REQUEST,
TRANSACTION_PAGE_LOAD,
} from '../../../common/transaction_types';
-import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values';
+import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es';
interface Options {
setup: Setup & SetupTimeRange;
@@ -43,30 +42,14 @@ export async function getServiceMapServiceNodeInfo({
}: Options & { serviceName: string; environment?: string }) {
const { start, end } = setup;
- const environmentNotDefinedFilter = {
- bool: { must_not: [{ exists: { field: SERVICE_ENVIRONMENT } }] },
- };
-
const filter: ESFilter[] = [
{ range: rangeFilter(start, end) },
{ term: { [SERVICE_NAME]: serviceName } },
+ ...getEnvironmentUiFilterES(environment),
];
- if (environment) {
- filter.push(
- environment === ENVIRONMENT_NOT_DEFINED
- ? environmentNotDefinedFilter
- : { term: { [SERVICE_ENVIRONMENT]: environment } }
- );
- }
-
const minutes = Math.abs((end - start) / (1000 * 60));
-
- const taskParams = {
- setup,
- minutes,
- filter,
- };
+ const taskParams = { setup, minutes, filter };
const [
errorMetrics,
@@ -97,11 +80,7 @@ async function getErrorMetrics({ setup, minutes, filter }: TaskParameters) {
size: 0,
query: {
bool: {
- filter: filter.concat({
- term: {
- [PROCESSOR_EVENT]: 'error',
- },
- }),
+ filter: filter.concat({ term: { [PROCESSOR_EVENT]: 'error' } }),
},
},
track_total_hits: true,
@@ -134,11 +113,7 @@ async function getTransactionStats({
bool: {
filter: [
...filter,
- {
- term: {
- [PROCESSOR_EVENT]: 'transaction',
- },
- },
+ { term: { [PROCESSOR_EVENT]: 'transaction' } },
{
terms: {
[TRANSACTION_TYPE]: [
@@ -151,13 +126,7 @@ async function getTransactionStats({
},
},
track_total_hits: true,
- aggs: {
- duration: {
- avg: {
- field: TRANSACTION_DURATION,
- },
- },
- },
+ aggs: { duration: { avg: { field: TRANSACTION_DURATION } } },
},
};
const response = await client.search(params);
@@ -181,32 +150,16 @@ async function getCpuMetrics({
query: {
bool: {
filter: filter.concat([
- {
- term: {
- [PROCESSOR_EVENT]: 'metric',
- },
- },
- {
- exists: {
- field: METRIC_SYSTEM_CPU_PERCENT,
- },
- },
+ { term: { [PROCESSOR_EVENT]: 'metric' } },
+ { exists: { field: METRIC_SYSTEM_CPU_PERCENT } },
]),
},
},
- aggs: {
- avgCpuUsage: {
- avg: {
- field: METRIC_SYSTEM_CPU_PERCENT,
- },
- },
- },
+ aggs: { avgCpuUsage: { avg: { field: METRIC_SYSTEM_CPU_PERCENT } } },
},
});
- return {
- avgCpuUsage: response.aggregations?.avgCpuUsage.value ?? null,
- };
+ return { avgCpuUsage: response.aggregations?.avgCpuUsage.value ?? null };
}
async function getMemoryMetrics({
@@ -220,31 +173,13 @@ async function getMemoryMetrics({
query: {
bool: {
filter: filter.concat([
- {
- term: {
- [PROCESSOR_EVENT]: 'metric',
- },
- },
- {
- exists: {
- field: METRIC_SYSTEM_FREE_MEMORY,
- },
- },
- {
- exists: {
- field: METRIC_SYSTEM_TOTAL_MEMORY,
- },
- },
+ { term: { [PROCESSOR_EVENT]: 'metric' } },
+ { exists: { field: METRIC_SYSTEM_FREE_MEMORY } },
+ { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } },
]),
},
},
- aggs: {
- avgMemoryUsage: {
- avg: {
- script: percentMemoryUsedScript,
- },
- },
- },
+ aggs: { avgMemoryUsage: { avg: { script: percentMemoryUsedScript } } },
},
});
diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts
index 6da5d195cf194..6a8aaf8dca8a6 100644
--- a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts
+++ b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts
@@ -29,14 +29,9 @@ export async function getDerivedServiceAnnotations({
const filter: ESFilter[] = [
{ term: { [PROCESSOR_EVENT]: 'transaction' } },
{ term: { [SERVICE_NAME]: serviceName } },
+ ...getEnvironmentUiFilterES(environment),
];
- const environmentFilter = getEnvironmentUiFilterES(environment);
-
- if (environmentFilter) {
- filter.push(environmentFilter);
- }
-
const versions =
(
await client.search({
diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts
index 75aeb27ea2122..6e3ae0181ddee 100644
--- a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts
+++ b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts
@@ -29,8 +29,6 @@ export async function getStoredAnnotations({
logger: Logger;
}): Promise {
try {
- const environmentFilter = getEnvironmentUiFilterES(environment);
-
const response: ESSearchResponse = (await apiCaller(
'search',
{
@@ -51,7 +49,7 @@ export async function getStoredAnnotations({
{ term: { 'annotation.type': 'deployment' } },
{ term: { tags: 'apm' } },
{ term: { [SERVICE_NAME]: serviceName } },
- ...(environmentFilter ? [environmentFilter] : []),
+ ...getEnvironmentUiFilterES(environment),
],
},
},
From 4925a4983a74572b2fba84f46111f9d04225950e Mon Sep 17 00:00:00 2001
From: Luke Elmers
Date: Mon, 13 Jul 2020 09:11:11 -0600
Subject: [PATCH 05/40] Ensure Other bucket works on scripted fields. (#71329)
---
.../_terms_other_bucket_helper.test.ts | 77 +++++++++++++++++++
.../buckets/_terms_other_bucket_helper.ts | 10 ++-
2 files changed, 83 insertions(+), 4 deletions(-)
diff --git a/src/plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.test.ts b/src/plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.test.ts
index 8e862b5692ca3..e9b4629ba88cf 100644
--- a/src/plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.test.ts
+++ b/src/plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.test.ts
@@ -316,6 +316,83 @@ describe('Terms Agg Other bucket helper', () => {
}
});
+ test('excludes exists filter for scripted fields', () => {
+ const aggConfigs = getAggConfigs(nestedTerm.aggs);
+ aggConfigs.aggs[1].params.field.scripted = true;
+ const agg = buildOtherBucketAgg(
+ aggConfigs,
+ aggConfigs.aggs[1] as IBucketAggConfig,
+ nestedTermResponse
+ );
+ const expectedResponse = {
+ 'other-filter': {
+ aggs: undefined,
+ filters: {
+ filters: {
+ '-IN': {
+ bool: {
+ must: [],
+ filter: [{ match_phrase: { 'geo.src': 'IN' } }],
+ should: [],
+ must_not: [
+ {
+ script: {
+ script: {
+ lang: undefined,
+ params: { value: 'ios' },
+ source: '(undefined) == value',
+ },
+ },
+ },
+ {
+ script: {
+ script: {
+ lang: undefined,
+ params: { value: 'win xp' },
+ source: '(undefined) == value',
+ },
+ },
+ },
+ ],
+ },
+ },
+ '-US': {
+ bool: {
+ must: [],
+ filter: [{ match_phrase: { 'geo.src': 'US' } }],
+ should: [],
+ must_not: [
+ {
+ script: {
+ script: {
+ lang: undefined,
+ params: { value: 'ios' },
+ source: '(undefined) == value',
+ },
+ },
+ },
+ {
+ script: {
+ script: {
+ lang: undefined,
+ params: { value: 'win xp' },
+ source: '(undefined) == value',
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ };
+ expect(agg).toBeDefined();
+ if (agg) {
+ expect(agg()).toEqual(expectedResponse);
+ }
+ });
+
test('returns false when nested terms agg has no buckets', () => {
const aggConfigs = getAggConfigs(nestedTerm.aggs);
const agg = buildOtherBucketAgg(
diff --git a/src/plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.ts b/src/plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.ts
index fba3d35f002af..1a7deafb548ae 100644
--- a/src/plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.ts
+++ b/src/plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.ts
@@ -202,10 +202,12 @@ export const buildOtherBucketAgg = (
return;
}
- if (
- !aggWithOtherBucket.params.missingBucket ||
- agg.buckets.some((bucket: { key: string }) => bucket.key === '__missing__')
- ) {
+ const hasScriptedField = !!aggWithOtherBucket.params.field.scripted;
+ const hasMissingBucket = !!aggWithOtherBucket.params.missingBucket;
+ const hasMissingBucketKey = agg.buckets.some(
+ (bucket: { key: string }) => bucket.key === '__missing__'
+ );
+ if (!hasScriptedField && (!hasMissingBucket || hasMissingBucketKey)) {
filters.push(
buildExistsFilter(
aggWithOtherBucket.params.field,
From 82f6c6a1df9168b104227e999d927ea500bb11cc Mon Sep 17 00:00:00 2001
From: Jen Huang
Date: Mon, 13 Jul 2020 08:12:53 -0700
Subject: [PATCH 06/40] [Ingest Manager] Simplify add/edit package config
(integration) form (#71187)
* Match add integration page with designs
* Clean up package config layout code
* Match edit integration config page with designs
* Fix typing and i18n issues
* Add back data test subj
* Add loading UI for second step; code clean up
* Fix limited packages incorrect response
* Add ability to create agent config when selecting config to add integration to
* Add error count to input-level panel; memoize children components
* Add error count next to all advanced options toggles
* Move general form error to bottom bar
* #69750 Auto-expand inputs with required & empty (invalid) vars
* #68019 Enforce unique package config names, per agent config
* Fix typing
* Fix i18n
* Fix reloading when new agent config _wasn't_ created
* Memoize edit integration and fix fields not collapsing on edit
* Really fix types
---
.../components/layout.tsx | 252 +++++++-------
.../package_config_input_config.tsx | 281 ++++++++--------
.../components/package_config_input_panel.tsx | 317 +++++++++---------
.../package_config_input_stream.tsx | 293 ++++++++--------
.../package_config_input_var_field.tsx | 12 +-
.../create_package_config_page/index.tsx | 213 +++++++-----
.../has_invalid_but_required_var.test.ts | 94 ++++++
.../services/has_invalid_but_required_var.ts | 26 ++
.../services/index.ts | 3 +
.../services/is_advanced_var.ts | 2 +-
.../services/validate_package_config.ts | 17 +-
.../step_configure_package.tsx | 120 +++----
.../step_define_package_config.tsx | 228 +++++++------
.../step_select_config.tsx | 231 ++++++++-----
.../step_select_package.tsx | 25 +-
.../edit_package_config_page/index.tsx | 191 ++++++-----
.../list_page/components/create_config.tsx | 8 +-
.../ingest_manager/types/index.ts | 1 +
.../server/services/package_config.ts | 29 ++
.../translations/translations/ja-JP.json | 10 -
.../translations/translations/zh-CN.json | 10 -
.../apis/index.js | 1 +
.../apis/package_config/create.ts | 43 +++
.../apis/package_config/update.ts | 127 +++++++
24 files changed, 1506 insertions(+), 1028 deletions(-)
create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/has_invalid_but_required_var.test.ts
create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/has_invalid_but_required_var.ts
create mode 100644 x-pack/test/ingest_manager_api_integration/apis/package_config/update.ts
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/layout.tsx
index e0f40f1b15375..7ccb59f0e741e 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/layout.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/layout.tsx
@@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import React, { memo, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFlexGroup,
@@ -27,130 +27,148 @@ export const CreatePackageConfigPageLayout: React.FunctionComponent<{
agentConfig?: AgentConfig;
packageInfo?: PackageInfo;
'data-test-subj'?: string;
-}> = ({
- from,
- cancelUrl,
- onCancel,
- agentConfig,
- packageInfo,
- children,
- 'data-test-subj': dataTestSubj,
-}) => {
- const leftColumn = (
-
-
- {/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
-
-
-
-
-
+}> = memo(
+ ({
+ from,
+ cancelUrl,
+ onCancel,
+ agentConfig,
+ packageInfo,
+ children,
+ 'data-test-subj': dataTestSubj,
+ }) => {
+ const pageTitle = useMemo(() => {
+ if ((from === 'package' || from === 'edit') && packageInfo) {
+ return (
+
+
+
+
+
+
+
+ {from === 'edit' ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ );
+ }
+
+ return from === 'edit' ? (
- {from === 'edit' ? (
-
- ) : (
-
- )}
+
-
-
-
-
- {from === 'edit' ? (
+ ) : (
+
+
- ) : from === 'config' ? (
+
+
+ );
+ }, [from, packageInfo]);
+
+ const pageDescription = useMemo(() => {
+ return from === 'edit' ? (
+
+ ) : from === 'config' ? (
+
+ ) : (
+
+ );
+ }, [from]);
+
+ const leftColumn = (
+
+
+ {/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
+
- ) : (
+
+
+ {pageTitle}
+
+
+
+ {pageDescription}
+
+
+
+ );
+
+ const rightColumn =
+ agentConfig && (from === 'config' || from === 'edit') ? (
+
+
- )}
-
-
-
- );
- const rightColumn = (
-
-
-
- {agentConfig && (from === 'config' || from === 'edit') ? (
-
-
-
-
-
- {agentConfig?.name || '-'}
-
-
- ) : null}
- {packageInfo && from === 'package' ? (
-
-
-
-
-
-
-
-
-
-
- {packageInfo?.title || packageInfo?.name || '-'}
-
-
-
-
- ) : null}
-
-
- );
+
+ {agentConfig?.name || '-'}
+
+ ) : undefined;
- const maxWidth = 770;
- return (
-
- {children}
-
- );
-};
+ const maxWidth = 770;
+ return (
+
+ {children}
+
+ );
+ }
+);
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_config.tsx
index 85c0f2134d8dc..98f04dbd92659 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_config.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_config.tsx
@@ -3,17 +3,15 @@
* 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, Fragment } from 'react';
+import React, { useState, Fragment, memo, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
+ EuiFlexGrid,
EuiFlexGroup,
EuiFlexItem,
EuiText,
- EuiTextColor,
EuiSpacer,
EuiButtonEmpty,
- EuiTitle,
- EuiIconTip,
} from '@elastic/eui';
import { PackageConfigInput, RegistryVarsEntry } from '../../../../types';
import {
@@ -29,150 +27,157 @@ export const PackageConfigInputConfig: React.FunctionComponent<{
updatePackageConfigInput: (updatedInput: Partial) => void;
inputVarsValidationResults: PackageConfigConfigValidationResults;
forceShowErrors?: boolean;
-}> = ({
- packageInputVars,
- packageConfigInput,
- updatePackageConfigInput,
- inputVarsValidationResults,
- forceShowErrors,
-}) => {
- // Showing advanced options toggle state
- const [isShowingAdvanced, setIsShowingAdvanced] = useState(false);
+}> = memo(
+ ({
+ packageInputVars,
+ packageConfigInput,
+ updatePackageConfigInput,
+ inputVarsValidationResults,
+ forceShowErrors,
+ }) => {
+ // Showing advanced options toggle state
+ const [isShowingAdvanced, setIsShowingAdvanced] = useState(false);
- // Errors state
- const hasErrors = forceShowErrors && validationHasErrors(inputVarsValidationResults);
+ // Errors state
+ const hasErrors = forceShowErrors && validationHasErrors(inputVarsValidationResults);
- const requiredVars: RegistryVarsEntry[] = [];
- const advancedVars: RegistryVarsEntry[] = [];
+ const requiredVars: RegistryVarsEntry[] = [];
+ const advancedVars: RegistryVarsEntry[] = [];
- if (packageInputVars) {
- packageInputVars.forEach((varDef) => {
- if (isAdvancedVar(varDef)) {
- advancedVars.push(varDef);
- } else {
- requiredVars.push(varDef);
- }
- });
- }
+ if (packageInputVars) {
+ packageInputVars.forEach((varDef) => {
+ if (isAdvancedVar(varDef)) {
+ advancedVars.push(varDef);
+ } else {
+ requiredVars.push(varDef);
+ }
+ });
+ }
+
+ const advancedVarsWithErrorsCount: number = useMemo(
+ () =>
+ advancedVars.filter(
+ ({ name: varName }) => inputVarsValidationResults.vars?.[varName]?.length
+ ).length,
+ [advancedVars, inputVarsValidationResults.vars]
+ );
- return (
-
-
-
-
-
-
-
+ return (
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
- {hasErrors ? (
-
-
- }
- position="right"
- type="alert"
- iconProps={{ color: 'danger' }}
- />
-
- ) : null}
-
-
-
-
-
-
-
-
-
-
- {requiredVars.map((varDef) => {
- const { name: varName, type: varType } = varDef;
- const value = packageConfigInput.vars![varName].value;
- return (
-
- {
- updatePackageConfigInput({
- vars: {
- ...packageConfigInput.vars,
- [varName]: {
- type: varType,
- value: newValue,
+
+
+
+ {requiredVars.map((varDef) => {
+ const { name: varName, type: varType } = varDef;
+ const value = packageConfigInput.vars![varName].value;
+ return (
+
+ {
+ updatePackageConfigInput({
+ vars: {
+ ...packageConfigInput.vars,
+ [varName]: {
+ type: varType,
+ value: newValue,
+ },
},
- },
- });
- }}
- errors={inputVarsValidationResults.vars![varName]}
- forceShowErrors={forceShowErrors}
- />
-
- );
- })}
- {advancedVars.length ? (
-
-
- {/* Wrapper div to prevent button from going full width */}
-
- setIsShowingAdvanced(!isShowingAdvanced)}
- flush="left"
- >
-
-
-
-
- {isShowingAdvanced
- ? advancedVars.map((varDef) => {
- const { name: varName, type: varType } = varDef;
- const value = packageConfigInput.vars![varName].value;
- return (
-
- {
- updatePackageConfigInput({
- vars: {
- ...packageConfigInput.vars,
- [varName]: {
- type: varType,
- value: newValue,
- },
- },
- });
- }}
- errors={inputVarsValidationResults.vars![varName]}
- forceShowErrors={forceShowErrors}
+ });
+ }}
+ errors={inputVarsValidationResults.vars![varName]}
+ forceShowErrors={forceShowErrors}
+ />
+
+ );
+ })}
+ {advancedVars.length ? (
+
+
+ {/* Wrapper div to prevent button from going full width */}
+
+
+ setIsShowingAdvanced(!isShowingAdvanced)}
+ flush="left"
+ >
+
+
+
+ {!isShowingAdvanced && hasErrors && advancedVarsWithErrorsCount ? (
+
+
+
+
- );
- })
- : null}
-
- ) : null}
-
-
-
- );
-};
+ ) : null}
+
+
+ {isShowingAdvanced
+ ? advancedVars.map((varDef) => {
+ const { name: varName, type: varType } = varDef;
+ const value = packageConfigInput.vars![varName].value;
+ return (
+
+ {
+ updatePackageConfigInput({
+ vars: {
+ ...packageConfigInput.vars,
+ [varName]: {
+ type: varType,
+ value: newValue,
+ },
+ },
+ });
+ }}
+ errors={inputVarsValidationResults.vars![varName]}
+ forceShowErrors={forceShowErrors}
+ />
+
+ );
+ })
+ : null}
+
+ ) : null}
+
+
+
+ );
+ }
+);
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_panel.tsx
index f9c9dcd469b25..af26afdbf74d7 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_panel.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_panel.tsx
@@ -3,21 +3,18 @@
* 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, Fragment } from 'react';
+import React, { useState, Fragment, memo } from 'react';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
- EuiPanel,
EuiFlexGroup,
EuiFlexItem,
EuiSwitch,
EuiText,
- EuiTextColor,
EuiButtonIcon,
EuiHorizontalRule,
EuiSpacer,
- EuiIconTip,
} from '@elastic/eui';
import {
PackageConfigInput,
@@ -25,16 +22,44 @@ import {
RegistryInput,
RegistryStream,
} from '../../../../types';
-import { PackageConfigInputValidationResults, validationHasErrors } from '../services';
+import {
+ PackageConfigInputValidationResults,
+ hasInvalidButRequiredVar,
+ countValidationErrors,
+} from '../services';
import { PackageConfigInputConfig } from './package_config_input_config';
import { PackageConfigInputStreamConfig } from './package_config_input_stream';
-const FlushHorizontalRule = styled(EuiHorizontalRule)`
- margin-left: -${(props) => props.theme.eui.paddingSizes.m};
- margin-right: -${(props) => props.theme.eui.paddingSizes.m};
- width: auto;
+const ShortenedHorizontalRule = styled(EuiHorizontalRule)`
+ &&& {
+ width: ${(11 / 12) * 100}%;
+ margin-left: auto;
+ }
`;
+const shouldShowStreamsByDefault = (
+ packageInput: RegistryInput,
+ packageInputStreams: Array,
+ packageConfigInput: PackageConfigInput
+): boolean => {
+ return (
+ packageConfigInput.enabled &&
+ (hasInvalidButRequiredVar(packageInput.vars, packageConfigInput.vars) ||
+ Boolean(
+ packageInputStreams.find(
+ (stream) =>
+ stream.enabled &&
+ hasInvalidButRequiredVar(
+ stream.vars,
+ packageConfigInput.streams.find(
+ (pkgStream) => stream.dataset.name === pkgStream.dataset.name
+ )?.vars
+ )
+ )
+ ))
+ );
+};
+
export const PackageConfigInputPanel: React.FunctionComponent<{
packageInput: RegistryInput;
packageInputStreams: Array;
@@ -42,148 +67,136 @@ export const PackageConfigInputPanel: React.FunctionComponent<{
updatePackageConfigInput: (updatedInput: Partial) => void;
inputValidationResults: PackageConfigInputValidationResults;
forceShowErrors?: boolean;
-}> = ({
- packageInput,
- packageInputStreams,
- packageConfigInput,
- updatePackageConfigInput,
- inputValidationResults,
- forceShowErrors,
-}) => {
- // Showing streams toggle state
- const [isShowingStreams, setIsShowingStreams] = useState(false);
+}> = memo(
+ ({
+ packageInput,
+ packageInputStreams,
+ packageConfigInput,
+ updatePackageConfigInput,
+ inputValidationResults,
+ forceShowErrors,
+ }) => {
+ // Showing streams toggle state
+ const [isShowingStreams, setIsShowingStreams] = useState(
+ shouldShowStreamsByDefault(packageInput, packageInputStreams, packageConfigInput)
+ );
- // Errors state
- const hasErrors = forceShowErrors && validationHasErrors(inputValidationResults);
+ // Errors state
+ const errorCount = countValidationErrors(inputValidationResults);
+ const hasErrors = forceShowErrors && errorCount;
- return (
-
- {/* Header / input-level toggle */}
-
-
-
-
-
-
-
- {packageInput.title || packageInput.type}
-
-
-
-
- {hasErrors ? (
+ const inputStreams = packageInputStreams
+ .map((packageInputStream) => {
+ return {
+ packageInputStream,
+ packageConfigInputStream: packageConfigInput.streams.find(
+ (stream) => stream.dataset.name === packageInputStream.dataset.name
+ ),
+ };
+ })
+ .filter((stream) => Boolean(stream.packageConfigInputStream));
+
+ return (
+ <>
+ {/* Header / input-level toggle */}
+
+
+
-
- }
- position="right"
- type="alert"
- iconProps={{ color: 'danger' }}
- />
+
+ {packageInput.title || packageInput.type}
+
- ) : null}
-
- }
- checked={packageConfigInput.enabled}
- onChange={(e) => {
- const enabled = e.target.checked;
- updatePackageConfigInput({
- enabled,
- streams: packageConfigInput.streams.map((stream) => ({
- ...stream,
+
+ }
+ checked={packageConfigInput.enabled}
+ onChange={(e) => {
+ const enabled = e.target.checked;
+ updatePackageConfigInput({
enabled,
- })),
- });
- }}
- />
-
-
-
-
-
-
-
- {packageConfigInput.streams.filter((stream) => stream.enabled).length}
-
-
- ),
- total: packageInputStreams.length,
- }}
- />
-
-
-
- setIsShowingStreams(!isShowingStreams)}
- color="text"
- aria-label={
- isShowingStreams
- ? i18n.translate(
- 'xpack.ingestManager.createPackageConfig.stepConfigure.hideStreamsAriaLabel',
- {
- defaultMessage: 'Hide {type} streams',
- values: {
- type: packageInput.type,
- },
- }
- )
- : i18n.translate(
- 'xpack.ingestManager.createPackageConfig.stepConfigure.showStreamsAriaLabel',
- {
- defaultMessage: 'Show {type} streams',
- values: {
- type: packageInput.type,
- },
- }
- )
+ streams: packageConfigInput.streams.map((stream) => ({
+ ...stream,
+ enabled,
+ })),
+ });
+ if (!enabled && isShowingStreams) {
+ setIsShowingStreams(false);
}
- />
-
-
-
-
+ }}
+ />
+
+
+
+ {hasErrors ? (
+
+
+
+
+
+ ) : null}
+
+ setIsShowingStreams(!isShowingStreams)}
+ color={hasErrors ? 'danger' : 'text'}
+ aria-label={
+ isShowingStreams
+ ? i18n.translate(
+ 'xpack.ingestManager.createPackageConfig.stepConfigure.hideStreamsAriaLabel',
+ {
+ defaultMessage: 'Hide {type} inputs',
+ values: {
+ type: packageInput.type,
+ },
+ }
+ )
+ : i18n.translate(
+ 'xpack.ingestManager.createPackageConfig.stepConfigure.showStreamsAriaLabel',
+ {
+ defaultMessage: 'Show {type} inputs',
+ values: {
+ type: packageInput.type,
+ },
+ }
+ )
+ }
+ />
+
+
+
+
- {/* Header rule break */}
- {isShowingStreams ? : null}
+ {/* Header rule break */}
+ {isShowingStreams ? : null}
- {/* Input level configuration */}
- {isShowingStreams && packageInput.vars && packageInput.vars.length ? (
-
-
-
-
- ) : null}
+ {/* Input level configuration */}
+ {isShowingStreams && packageInput.vars && packageInput.vars.length ? (
+
+
+
+
+ ) : null}
- {/* Per-stream configuration */}
- {isShowingStreams ? (
-
- {packageInputStreams.map((packageInputStream) => {
- const packageConfigInputStream = packageConfigInput.streams.find(
- (stream) => stream.dataset.name === packageInputStream.dataset.name
- );
- return packageConfigInputStream ? (
-
+ {/* Per-stream configuration */}
+ {isShowingStreams ? (
+
+ {inputStreams.map(({ packageInputStream, packageConfigInputStream }, index) => (
+
) => {
@@ -213,17 +226,21 @@ export const PackageConfigInputPanel: React.FunctionComponent<{
updatePackageConfigInput(updatedInput);
}}
inputStreamValidationResults={
- inputValidationResults.streams![packageConfigInputStream.id]
+ inputValidationResults.streams![packageConfigInputStream!.id]
}
forceShowErrors={forceShowErrors}
/>
-
-
+ {index !== inputStreams.length - 1 ? (
+ <>
+
+
+ >
+ ) : null}
- ) : null;
- })}
-
- ) : null}
-
- );
-};
+ ))}
+
+ ) : null}
+ >
+ );
+ }
+);
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_stream.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_stream.tsx
index 52a4748fe14c7..11a9df276485b 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_stream.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_stream.tsx
@@ -3,18 +3,17 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useState, Fragment } from 'react';
+import React, { useState, Fragment, memo, useMemo } from 'react';
import ReactMarkdown from 'react-markdown';
import { FormattedMessage } from '@kbn/i18n/react';
import {
+ EuiFlexGrid,
EuiFlexGroup,
EuiFlexItem,
EuiSwitch,
EuiText,
EuiSpacer,
EuiButtonEmpty,
- EuiTextColor,
- EuiIconTip,
} from '@elastic/eui';
import { PackageConfigInputStream, RegistryStream, RegistryVarsEntry } from '../../../../types';
import {
@@ -30,153 +29,157 @@ export const PackageConfigInputStreamConfig: React.FunctionComponent<{
updatePackageConfigInputStream: (updatedStream: Partial) => void;
inputStreamValidationResults: PackageConfigConfigValidationResults;
forceShowErrors?: boolean;
-}> = ({
- packageInputStream,
- packageConfigInputStream,
- updatePackageConfigInputStream,
- inputStreamValidationResults,
- forceShowErrors,
-}) => {
- // Showing advanced options toggle state
- const [isShowingAdvanced, setIsShowingAdvanced] = useState(false);
+}> = memo(
+ ({
+ packageInputStream,
+ packageConfigInputStream,
+ updatePackageConfigInputStream,
+ inputStreamValidationResults,
+ forceShowErrors,
+ }) => {
+ // Showing advanced options toggle state
+ const [isShowingAdvanced, setIsShowingAdvanced] = useState();
- // Errors state
- const hasErrors = forceShowErrors && validationHasErrors(inputStreamValidationResults);
+ // Errors state
+ const hasErrors = forceShowErrors && validationHasErrors(inputStreamValidationResults);
- const requiredVars: RegistryVarsEntry[] = [];
- const advancedVars: RegistryVarsEntry[] = [];
+ const requiredVars: RegistryVarsEntry[] = [];
+ const advancedVars: RegistryVarsEntry[] = [];
- if (packageInputStream.vars && packageInputStream.vars.length) {
- packageInputStream.vars.forEach((varDef) => {
- if (isAdvancedVar(varDef)) {
- advancedVars.push(varDef);
- } else {
- requiredVars.push(varDef);
- }
- });
- }
+ if (packageInputStream.vars && packageInputStream.vars.length) {
+ packageInputStream.vars.forEach((varDef) => {
+ if (isAdvancedVar(varDef)) {
+ advancedVars.push(varDef);
+ } else {
+ requiredVars.push(varDef);
+ }
+ });
+ }
- return (
-
-
-
-
-
- {packageInputStream.title}
-
-
- {hasErrors ? (
-
-
- }
- position="right"
- type="alert"
- iconProps={{ color: 'danger' }}
- />
-
+ const advancedVarsWithErrorsCount: number = useMemo(
+ () =>
+ advancedVars.filter(
+ ({ name: varName }) => inputStreamValidationResults.vars?.[varName]?.length
+ ).length,
+ [advancedVars, inputStreamValidationResults.vars]
+ );
+
+ return (
+
+
+
+
+
+ {
+ const enabled = e.target.checked;
+ updatePackageConfigInputStream({
+ enabled,
+ });
+ }}
+ />
+ {packageInputStream.description ? (
+
+
+
+
+
+
) : null}
-
- }
- checked={packageConfigInputStream.enabled}
- onChange={(e) => {
- const enabled = e.target.checked;
- updatePackageConfigInputStream({
- enabled,
- });
- }}
- />
- {packageInputStream.description ? (
-
-
-
-
-
-
- ) : null}
-
-
-
- {requiredVars.map((varDef) => {
- const { name: varName, type: varType } = varDef;
- const value = packageConfigInputStream.vars![varName].value;
- return (
-
- {
- updatePackageConfigInputStream({
- vars: {
- ...packageConfigInputStream.vars,
- [varName]: {
- type: varType,
- value: newValue,
+
+
+
+
+
+ {requiredVars.map((varDef) => {
+ const { name: varName, type: varType } = varDef;
+ const value = packageConfigInputStream.vars![varName].value;
+ return (
+
+ {
+ updatePackageConfigInputStream({
+ vars: {
+ ...packageConfigInputStream.vars,
+ [varName]: {
+ type: varType,
+ value: newValue,
+ },
},
- },
- });
- }}
- errors={inputStreamValidationResults.vars![varName]}
- forceShowErrors={forceShowErrors}
- />
-
- );
- })}
- {advancedVars.length ? (
-
-
- {/* Wrapper div to prevent button from going full width */}
-
- setIsShowingAdvanced(!isShowingAdvanced)}
- flush="left"
- >
-
-
-
-
- {isShowingAdvanced
- ? advancedVars.map((varDef) => {
- const { name: varName, type: varType } = varDef;
- const value = packageConfigInputStream.vars![varName].value;
- return (
-
- {
- updatePackageConfigInputStream({
- vars: {
- ...packageConfigInputStream.vars,
- [varName]: {
- type: varType,
- value: newValue,
- },
- },
- });
- }}
- errors={inputStreamValidationResults.vars![varName]}
- forceShowErrors={forceShowErrors}
+ });
+ }}
+ errors={inputStreamValidationResults.vars![varName]}
+ forceShowErrors={forceShowErrors}
+ />
+
+ );
+ })}
+ {advancedVars.length ? (
+
+
+
+
+ setIsShowingAdvanced(!isShowingAdvanced)}
+ flush="left"
+ >
+
+
+
+ {!isShowingAdvanced && hasErrors && advancedVarsWithErrorsCount ? (
+
+
+
+
- );
- })
- : null}
-
- ) : null}
-
-
-
- );
-};
+ ) : null}
+
+
+ {isShowingAdvanced
+ ? advancedVars.map((varDef) => {
+ const { name: varName, type: varType } = varDef;
+ const value = packageConfigInputStream.vars![varName].value;
+ return (
+
+ {
+ updatePackageConfigInputStream({
+ vars: {
+ ...packageConfigInputStream.vars,
+ [varName]: {
+ type: varType,
+ value: newValue,
+ },
+ },
+ });
+ }}
+ errors={inputStreamValidationResults.vars![varName]}
+ forceShowErrors={forceShowErrors}
+ />
+
+ );
+ })
+ : null}
+
+ ) : null}
+
+
+
+ );
+ }
+);
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_var_field.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_var_field.tsx
index 8868e00ecc1f1..eb681096a080e 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_var_field.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_var_field.tsx
@@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useState } from 'react';
+import React, { useState, memo, useMemo } from 'react';
import ReactMarkdown from 'react-markdown';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFormRow, EuiFieldText, EuiComboBox, EuiText, EuiCodeEditor } from '@elastic/eui';
@@ -18,13 +18,13 @@ export const PackageConfigInputVarField: React.FunctionComponent<{
onChange: (newValue: any) => void;
errors?: string[] | null;
forceShowErrors?: boolean;
-}> = ({ varDef, value, onChange, errors: varErrors, forceShowErrors }) => {
+}> = memo(({ varDef, value, onChange, errors: varErrors, forceShowErrors }) => {
const [isDirty, setIsDirty] = useState(false);
const { multi, required, type, title, name, description } = varDef;
const isInvalid = (isDirty || forceShowErrors) && !!varErrors;
const errors = isInvalid ? varErrors : null;
- const renderField = () => {
+ const field = useMemo(() => {
if (multi) {
return (
setIsDirty(true)}
/>
);
- };
+ }, [isInvalid, multi, onChange, type, value]);
return (
}
>
- {renderField()}
+ {field}
);
-};
+});
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/index.tsx
index b446e6bf97e7b..74cbcdca512db 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/index.tsx
@@ -5,6 +5,7 @@
*/
import React, { useState, useEffect, useMemo, useCallback, ReactEventHandler } from 'react';
import { useRouteMatch, useHistory } from 'react-router-dom';
+import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
@@ -31,6 +32,7 @@ import {
useConfig,
sendGetAgentStatus,
} from '../../../hooks';
+import { Loading } from '../../../components';
import { ConfirmDeployConfigModal } from '../components';
import { CreatePackageConfigPageLayout } from './components';
import { CreatePackageConfigFrom, PackageConfigFormState } from './types';
@@ -45,6 +47,12 @@ import { StepConfigurePackage } from './step_configure_package';
import { StepDefinePackageConfig } from './step_define_package_config';
import { useIntraAppState } from '../../../hooks/use_intra_app_state';
+const StepsWithLessPadding = styled(EuiSteps)`
+ .euiStep__content {
+ padding-bottom: ${(props) => props.theme.eui.paddingSizes.m};
+ }
+`;
+
export const CreatePackageConfigPage: React.FunctionComponent = () => {
const {
notifications,
@@ -75,6 +83,7 @@ export const CreatePackageConfigPage: React.FunctionComponent = () => {
// Agent config and package info states
const [agentConfig, setAgentConfig] = useState();
const [packageInfo, setPackageInfo] = useState();
+ const [isLoadingSecondStep, setIsLoadingSecondStep] = useState(false);
const agentConfigId = agentConfig?.id;
// Retrieve agent count
@@ -151,40 +160,47 @@ export const CreatePackageConfigPage: React.FunctionComponent = () => {
const hasErrors = validationResults ? validationHasErrors(validationResults) : false;
+ // Update package config validation
+ const updatePackageConfigValidation = useCallback(
+ (newPackageConfig?: NewPackageConfig) => {
+ if (packageInfo) {
+ const newValidationResult = validatePackageConfig(
+ newPackageConfig || packageConfig,
+ packageInfo
+ );
+ setValidationResults(newValidationResult);
+ // eslint-disable-next-line no-console
+ console.debug('Package config validation results', newValidationResult);
+
+ return newValidationResult;
+ }
+ },
+ [packageConfig, packageInfo]
+ );
+
// Update package config method
- const updatePackageConfig = (updatedFields: Partial) => {
- const newPackageConfig = {
- ...packageConfig,
- ...updatedFields,
- };
- setPackageConfig(newPackageConfig);
-
- // eslint-disable-next-line no-console
- console.debug('Package config updated', newPackageConfig);
- const newValidationResults = updatePackageConfigValidation(newPackageConfig);
- const hasPackage = newPackageConfig.package;
- const hasValidationErrors = newValidationResults
- ? validationHasErrors(newValidationResults)
- : false;
- const hasAgentConfig = newPackageConfig.config_id && newPackageConfig.config_id !== '';
- if (hasPackage && hasAgentConfig && !hasValidationErrors) {
- setFormState('VALID');
- }
- };
+ const updatePackageConfig = useCallback(
+ (updatedFields: Partial) => {
+ const newPackageConfig = {
+ ...packageConfig,
+ ...updatedFields,
+ };
+ setPackageConfig(newPackageConfig);
- const updatePackageConfigValidation = (newPackageConfig?: NewPackageConfig) => {
- if (packageInfo) {
- const newValidationResult = validatePackageConfig(
- newPackageConfig || packageConfig,
- packageInfo
- );
- setValidationResults(newValidationResult);
// eslint-disable-next-line no-console
- console.debug('Package config validation results', newValidationResult);
-
- return newValidationResult;
- }
- };
+ console.debug('Package config updated', newPackageConfig);
+ const newValidationResults = updatePackageConfigValidation(newPackageConfig);
+ const hasPackage = newPackageConfig.package;
+ const hasValidationErrors = newValidationResults
+ ? validationHasErrors(newValidationResults)
+ : false;
+ const hasAgentConfig = newPackageConfig.config_id && newPackageConfig.config_id !== '';
+ if (hasPackage && hasAgentConfig && !hasValidationErrors) {
+ setFormState('VALID');
+ }
+ },
+ [packageConfig, updatePackageConfigValidation]
+ );
// Cancel path
const cancelUrl = useMemo(() => {
@@ -276,6 +292,7 @@ export const CreatePackageConfigPage: React.FunctionComponent = () => {
updatePackageInfo={updatePackageInfo}
agentConfig={agentConfig}
updateAgentConfig={updateAgentConfig}
+ setIsLoadingSecondStep={setIsLoadingSecondStep}
/>
),
[pkgkey, updatePackageInfo, agentConfig, updateAgentConfig]
@@ -288,11 +305,47 @@ export const CreatePackageConfigPage: React.FunctionComponent = () => {
updateAgentConfig={updateAgentConfig}
packageInfo={packageInfo}
updatePackageInfo={updatePackageInfo}
+ setIsLoadingSecondStep={setIsLoadingSecondStep}
/>
),
[configId, updateAgentConfig, packageInfo, updatePackageInfo]
);
+ const stepConfigurePackage = useMemo(
+ () =>
+ isLoadingSecondStep ? (
+
+ ) : agentConfig && packageInfo ? (
+ <>
+
+
+ >
+ ) : (
+
+ ),
+ [
+ agentConfig,
+ formState,
+ isLoadingSecondStep,
+ packageConfig,
+ packageInfo,
+ updatePackageConfig,
+ validationResults,
+ ]
+ );
+
const steps: EuiStepProps[] = [
from === 'package'
? {
@@ -310,44 +363,16 @@ export const CreatePackageConfigPage: React.FunctionComponent = () => {
}),
children: stepSelectPackage,
},
- {
- title: i18n.translate(
- 'xpack.ingestManager.createPackageConfig.stepDefinePackageConfigTitle',
- {
- defaultMessage: 'Configure integration',
- }
- ),
- status: !packageInfo || !agentConfig ? 'disabled' : undefined,
- children:
- agentConfig && packageInfo ? (
-
- ) : null,
- },
{
title: i18n.translate(
'xpack.ingestManager.createPackageConfig.stepConfigurePackageConfigTitle',
{
- defaultMessage: 'Select the data you want to collect',
+ defaultMessage: 'Configure integration',
}
),
- status: !packageInfo || !agentConfig ? 'disabled' : undefined,
+ status: !packageInfo || !agentConfig || isLoadingSecondStep ? 'disabled' : undefined,
'data-test-subj': 'dataCollectionSetupStep',
- children:
- agentConfig && packageInfo ? (
-
- ) : null,
+ children: stepConfigurePackage,
},
];
@@ -371,7 +396,7 @@ export const CreatePackageConfigPage: React.FunctionComponent = () => {
: agentConfig && (
)}
-
+
{/* TODO #64541 - Remove classes */}
{
: undefined
}
>
-
+
- {/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
-
+ {!isLoadingSecondStep && agentConfig && packageInfo && formState === 'INVALID' ? (
-
+ ) : null}
-
-
-
+
+
+ {/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
+
+
+
+
+
+
+
+
+
+
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/has_invalid_but_required_var.test.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/has_invalid_but_required_var.test.ts
new file mode 100644
index 0000000000000..679ae4b1456d6
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/has_invalid_but_required_var.test.ts
@@ -0,0 +1,94 @@
+/*
+ * 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 { hasInvalidButRequiredVar } from './has_invalid_but_required_var';
+
+describe('Ingest Manager - hasInvalidButRequiredVar', () => {
+ it('returns true for invalid & required vars', () => {
+ expect(
+ hasInvalidButRequiredVar(
+ [
+ {
+ name: 'mock_var',
+ type: 'text',
+ required: true,
+ },
+ ],
+ {}
+ )
+ ).toBe(true);
+
+ expect(
+ hasInvalidButRequiredVar(
+ [
+ {
+ name: 'mock_var',
+ type: 'text',
+ required: true,
+ },
+ ],
+ {
+ mock_var: {
+ value: undefined,
+ },
+ }
+ )
+ ).toBe(true);
+ });
+
+ it('returns false for valid & required vars', () => {
+ expect(
+ hasInvalidButRequiredVar(
+ [
+ {
+ name: 'mock_var',
+ type: 'text',
+ required: true,
+ },
+ ],
+ {
+ mock_var: {
+ value: 'foo',
+ },
+ }
+ )
+ ).toBe(false);
+ });
+
+ it('returns false for optional vars', () => {
+ expect(
+ hasInvalidButRequiredVar(
+ [
+ {
+ name: 'mock_var',
+ type: 'text',
+ },
+ ],
+ {
+ mock_var: {
+ value: 'foo',
+ },
+ }
+ )
+ ).toBe(false);
+
+ expect(
+ hasInvalidButRequiredVar(
+ [
+ {
+ name: 'mock_var',
+ type: 'text',
+ required: false,
+ },
+ ],
+ {
+ mock_var: {
+ value: undefined,
+ },
+ }
+ )
+ ).toBe(false);
+ });
+});
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/has_invalid_but_required_var.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/has_invalid_but_required_var.ts
new file mode 100644
index 0000000000000..f632d40a05621
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/has_invalid_but_required_var.ts
@@ -0,0 +1,26 @@
+/*
+ * 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 { PackageConfigConfigRecord, RegistryVarsEntry } from '../../../../types';
+import { validatePackageConfigConfig } from './';
+
+export const hasInvalidButRequiredVar = (
+ registryVars?: RegistryVarsEntry[],
+ packageConfigVars?: PackageConfigConfigRecord
+): boolean => {
+ return (
+ (registryVars && !packageConfigVars) ||
+ Boolean(
+ registryVars &&
+ registryVars.find(
+ (registryVar) =>
+ registryVar.required &&
+ (!packageConfigVars ||
+ !packageConfigVars[registryVar.name] ||
+ validatePackageConfigConfig(packageConfigVars[registryVar.name], registryVar)?.length)
+ )
+ )
+ );
+};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/index.ts
index 6cfb1c74bd661..0d33a4e113f03 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/index.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/index.ts
@@ -4,10 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { isAdvancedVar } from './is_advanced_var';
+export { hasInvalidButRequiredVar } from './has_invalid_but_required_var';
export {
PackageConfigValidationResults,
PackageConfigConfigValidationResults,
PackageConfigInputValidationResults,
validatePackageConfig,
+ validatePackageConfigConfig,
validationHasErrors,
+ countValidationErrors,
} from './validate_package_config';
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/is_advanced_var.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/is_advanced_var.ts
index 398f1d675c5df..a2f4a6675ac80 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/is_advanced_var.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/is_advanced_var.ts
@@ -6,7 +6,7 @@
import { RegistryVarsEntry } from '../../../../types';
export const isAdvancedVar = (varDef: RegistryVarsEntry): boolean => {
- if (varDef.show_user || (varDef.required && !varDef.default)) {
+ if (varDef.show_user || (varDef.required && varDef.default === undefined)) {
return false;
}
return true;
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/validate_package_config.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/validate_package_config.ts
index cd301747c3f53..bd9d216ca969a 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/validate_package_config.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/validate_package_config.ts
@@ -171,7 +171,7 @@ export const validatePackageConfig = (
return validationResults;
};
-const validatePackageConfigConfig = (
+export const validatePackageConfigConfig = (
configEntry: PackageConfigConfigRecordEntry,
varDef: RegistryVarsEntry
): string[] | null => {
@@ -237,13 +237,22 @@ const validatePackageConfigConfig = (
return errors.length ? errors : null;
};
-export const validationHasErrors = (
+export const countValidationErrors = (
validationResults:
| PackageConfigValidationResults
| PackageConfigInputValidationResults
| PackageConfigConfigValidationResults
-) => {
+): number => {
const flattenedValidation = getFlattenedObject(validationResults);
+ const errors = Object.values(flattenedValidation).filter((value) => Boolean(value)) || [];
+ return errors.length;
+};
- return !!Object.entries(flattenedValidation).find(([, value]) => !!value);
+export const validationHasErrors = (
+ validationResults:
+ | PackageConfigValidationResults
+ | PackageConfigInputValidationResults
+ | PackageConfigConfigValidationResults
+): boolean => {
+ return countValidationErrors(validationResults) > 0;
};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_configure_package.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_configure_package.tsx
index eecd204a5e307..380a03e15695b 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_configure_package.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_configure_package.tsx
@@ -4,12 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiCallOut } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
+import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { PackageInfo, RegistryStream, NewPackageConfig, PackageConfigInput } from '../../../types';
import { Loading } from '../../../components';
-import { PackageConfigValidationResults, validationHasErrors } from './services';
+import { PackageConfigValidationResults } from './services';
import { PackageConfigInputPanel, CustomPackageConfig } from './components';
import { CreatePackageConfigFrom } from './types';
@@ -52,8 +50,6 @@ export const StepConfigurePackage: React.FunctionComponent<{
validationResults,
submitAttempted,
}) => {
- const hasErrors = validationResults ? validationHasErrors(validationResults) : false;
-
// Configure inputs (and their streams)
// Assume packages only export one config template for now
const renderConfigureInputs = () =>
@@ -61,76 +57,50 @@ export const StepConfigurePackage: React.FunctionComponent<{
packageInfo.config_templates[0] &&
packageInfo.config_templates[0].inputs &&
packageInfo.config_templates[0].inputs.length ? (
-
- {packageInfo.config_templates[0].inputs.map((packageInput) => {
- const packageConfigInput = packageConfig.inputs.find(
- (input) => input.type === packageInput.type
- );
- const packageInputStreams = findStreamsForInputType(packageInput.type, packageInfo);
- return packageConfigInput ? (
-
- ) => {
- const indexOfUpdatedInput = packageConfig.inputs.findIndex(
- (input) => input.type === packageInput.type
- );
- const newInputs = [...packageConfig.inputs];
- newInputs[indexOfUpdatedInput] = {
- ...newInputs[indexOfUpdatedInput],
- ...updatedInput,
- };
- updatePackageConfig({
- inputs: newInputs,
- });
- }}
- inputValidationResults={validationResults!.inputs![packageConfigInput.type]}
- forceShowErrors={submitAttempted}
- />
-
- ) : null;
- })}
-
+ <>
+
+
+ {packageInfo.config_templates[0].inputs.map((packageInput) => {
+ const packageConfigInput = packageConfig.inputs.find(
+ (input) => input.type === packageInput.type
+ );
+ const packageInputStreams = findStreamsForInputType(packageInput.type, packageInfo);
+ return packageConfigInput ? (
+
+ ) => {
+ const indexOfUpdatedInput = packageConfig.inputs.findIndex(
+ (input) => input.type === packageInput.type
+ );
+ const newInputs = [...packageConfig.inputs];
+ newInputs[indexOfUpdatedInput] = {
+ ...newInputs[indexOfUpdatedInput],
+ ...updatedInput,
+ };
+ updatePackageConfig({
+ inputs: newInputs,
+ });
+ }}
+ inputValidationResults={validationResults!.inputs![packageConfigInput.type]}
+ forceShowErrors={submitAttempted}
+ />
+
+
+ ) : null;
+ })}
+
+ >
) : (
-
-
-
+
);
- return validationResults ? (
-
- {renderConfigureInputs()}
- {hasErrors && submitAttempted ? (
-
-
-
-
-
-
-
-
-
- ) : null}
-
- ) : (
-
- );
+ return validationResults ? renderConfigureInputs() : ;
};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_define_package_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_define_package_config.tsx
index b2ffe62104eb1..a04d023ebcc48 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_define_package_config.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_define_package_config.tsx
@@ -3,17 +3,18 @@
* 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, { useEffect, useState, Fragment } from 'react';
+import React, { useEffect, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
- EuiFlexGrid,
- EuiFlexItem,
EuiFormRow,
EuiFieldText,
EuiButtonEmpty,
EuiSpacer,
EuiText,
EuiComboBox,
+ EuiDescribedFormGroup,
+ EuiFlexGroup,
+ EuiFlexItem,
} from '@elastic/eui';
import { AgentConfig, PackageInfo, PackageConfig, NewPackageConfig } from '../../../types';
import { packageToPackageConfigInputs } from '../../../services';
@@ -28,7 +29,7 @@ export const StepDefinePackageConfig: React.FunctionComponent<{
validationResults: PackageConfigValidationResults;
}> = ({ agentConfig, packageInfo, packageConfig, updatePackageConfig, validationResults }) => {
// Form show/hide states
- const [isShowingAdvancedDefine, setIsShowingAdvancedDefine] = useState(false);
+ const [isShowingAdvanced, setIsShowingAdvanced] = useState(false);
// Update package config's package and config info
useEffect(() => {
@@ -74,111 +75,140 @@ export const StepDefinePackageConfig: React.FunctionComponent<{
]);
return validationResults ? (
- <>
-
-
-
+
+
+
+ }
+ description={
+
+ }
+ >
+ <>
+ {/* Name */}
+
+ }
+ >
+
+ updatePackageConfig({
+ name: e.target.value,
+ })
}
- >
-
- updatePackageConfig({
- name: e.target.value,
- })
- }
- data-test-subj="packageConfigNameInput"
+ data-test-subj="packageConfigNameInput"
+ />
+
+
+ {/* Description */}
+
-
-
-
-
+
+ }
+ isInvalid={!!validationResults.description}
+ error={validationResults.description}
+ >
+
+ updatePackageConfig({
+ description: e.target.value,
+ })
}
- labelAppend={
-
+ />
+
+
+
+ {/* Advanced options toggle */}
+
+
+ setIsShowingAdvanced(!isShowingAdvanced)}
+ flush="left"
+ >
+
+
+
+ {!isShowingAdvanced && !!validationResults.namespace ? (
+
+
- }
- isInvalid={!!validationResults.description}
- error={validationResults.description}
- >
-
- updatePackageConfig({
- description: e.target.value,
- })
+
+ ) : null}
+
+
+ {/* Advanced options content */}
+ {/* Todo: Populate list of existing namespaces */}
+ {isShowingAdvanced ? (
+ <>
+
+
}
- />
-
-
-
-
- setIsShowingAdvancedDefine(!isShowingAdvancedDefine)}
- >
-
-
- {/* Todo: Populate list of existing namespaces */}
- {isShowingAdvancedDefine || !!validationResults.namespace ? (
-
-
-
-
-
+ >
+
- {
- updatePackageConfig({
- namespace: newNamespace,
- });
- }}
- onChange={(newNamespaces: Array<{ label: string }>) => {
- updatePackageConfig({
- namespace: newNamespaces.length ? newNamespaces[0].label : '',
- });
- }}
- />
-
-
-
-
- ) : null}
- >
+ onCreateOption={(newNamespace: string) => {
+ updatePackageConfig({
+ namespace: newNamespace,
+ });
+ }}
+ onChange={(newNamespaces: Array<{ label: string }>) => {
+ updatePackageConfig({
+ namespace: newNamespaces.length ? newNamespaces[0].label : '',
+ });
+ }}
+ />
+
+ >
+ ) : null}
+ >
+
) : (
);
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx
index f6391cf1fa456..d3120f9051f45 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx
@@ -6,29 +6,50 @@
import React, { useEffect, useState, Fragment } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiFlexGroup, EuiFlexItem, EuiSelectable, EuiSpacer, EuiTextColor } from '@elastic/eui';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSelectable,
+ EuiSpacer,
+ EuiTextColor,
+ EuiPortal,
+ EuiButtonEmpty,
+} from '@elastic/eui';
import { Error } from '../../../components';
import { AgentConfig, PackageInfo, GetAgentConfigsResponseItem } from '../../../types';
import { isPackageLimited, doesAgentConfigAlreadyIncludePackage } from '../../../services';
-import { useGetPackageInfoByKey, useGetAgentConfigs, sendGetOneAgentConfig } from '../../../hooks';
+import {
+ useGetPackageInfoByKey,
+ useGetAgentConfigs,
+ sendGetOneAgentConfig,
+ useCapabilities,
+} from '../../../hooks';
+import { CreateAgentConfigFlyout } from '../list_page/components';
export const StepSelectConfig: React.FunctionComponent<{
pkgkey: string;
updatePackageInfo: (packageInfo: PackageInfo | undefined) => void;
agentConfig: AgentConfig | undefined;
updateAgentConfig: (config: AgentConfig | undefined) => void;
-}> = ({ pkgkey, updatePackageInfo, agentConfig, updateAgentConfig }) => {
+ setIsLoadingSecondStep: (isLoading: boolean) => void;
+}> = ({ pkgkey, updatePackageInfo, agentConfig, updateAgentConfig, setIsLoadingSecondStep }) => {
// Selected config state
const [selectedConfigId, setSelectedConfigId] = useState(
agentConfig ? agentConfig.id : undefined
);
const [selectedConfigError, setSelectedConfigError] = useState();
+ // Create new config flyout state
+ const hasWriteCapabilites = useCapabilities().write;
+ const [isCreateAgentConfigFlyoutOpen, setIsCreateAgentConfigFlyoutOpen] = useState(
+ false
+ );
+
// Fetch package info
const {
data: packageInfoData,
error: packageInfoError,
- isLoading: packageInfoLoading,
+ isLoading: isPackageInfoLoading,
} = useGetPackageInfoByKey(pkgkey);
const isLimitedPackage = (packageInfoData && isPackageLimited(packageInfoData.response)) || false;
@@ -37,6 +58,7 @@ export const StepSelectConfig: React.FunctionComponent<{
data: agentConfigsData,
error: agentConfigsError,
isLoading: isAgentConfigsLoading,
+ sendRequest: refreshAgentConfigs,
} = useGetAgentConfigs({
page: 1,
perPage: 1000,
@@ -64,6 +86,7 @@ export const StepSelectConfig: React.FunctionComponent<{
useEffect(() => {
const fetchAgentConfigInfo = async () => {
if (selectedConfigId) {
+ setIsLoadingSecondStep(true);
const { data, error } = await sendGetOneAgentConfig(selectedConfigId);
if (error) {
setSelectedConfigError(error);
@@ -76,11 +99,12 @@ export const StepSelectConfig: React.FunctionComponent<{
setSelectedConfigError(undefined);
updateAgentConfig(undefined);
}
+ setIsLoadingSecondStep(false);
};
if (!agentConfig || selectedConfigId !== agentConfig.id) {
fetchAgentConfigInfo();
}
- }, [selectedConfigId, agentConfig, updateAgentConfig]);
+ }, [selectedConfigId, agentConfig, updateAgentConfig, setIsLoadingSecondStep]);
// Display package error if there is one
if (packageInfoError) {
@@ -113,92 +137,125 @@ export const StepSelectConfig: React.FunctionComponent<{
}
return (
-
-
- {
- const alreadyHasLimitedPackage =
- (isLimitedPackage &&
- packageInfoData &&
- doesAgentConfigAlreadyIncludePackage(agentConf, packageInfoData.response.name)) ||
- false;
- return {
- label: agentConf.name,
- key: agentConf.id,
- checked: selectedConfigId === agentConf.id ? 'on' : undefined,
- disabled: alreadyHasLimitedPackage,
- 'data-test-subj': 'agentConfigItem',
- };
- })}
- renderOption={(option) => (
-
- {option.label}
-
-
- {agentConfigsById[option.key!].description}
-
-
-
-
-
-
-
-
- )}
- listProps={{
- bordered: true,
- }}
- searchProps={{
- placeholder: i18n.translate(
- 'xpack.ingestManager.createPackageConfig.StepSelectConfig.filterAgentConfigsInputPlaceholder',
- {
- defaultMessage: 'Search for agent configurations',
+ <>
+ {isCreateAgentConfigFlyoutOpen ? (
+
+ {
+ setIsCreateAgentConfigFlyoutOpen(false);
+ if (newAgentConfig) {
+ refreshAgentConfigs();
+ setSelectedConfigId(newAgentConfig.id);
+ }
+ }}
+ />
+
+ ) : null}
+
+
+ {
+ const alreadyHasLimitedPackage =
+ (isLimitedPackage &&
+ packageInfoData &&
+ doesAgentConfigAlreadyIncludePackage(agentConf, packageInfoData.response.name)) ||
+ false;
+ return {
+ label: agentConf.name,
+ key: agentConf.id,
+ checked: selectedConfigId === agentConf.id ? 'on' : undefined,
+ disabled: alreadyHasLimitedPackage,
+ 'data-test-subj': 'agentConfigItem',
+ };
+ })}
+ renderOption={(option) => (
+
+ {option.label}
+
+
+ {agentConfigsById[option.key!].description}
+
+
+
+
+
+
+
+
+ )}
+ listProps={{
+ bordered: true,
+ }}
+ searchProps={{
+ placeholder: i18n.translate(
+ 'xpack.ingestManager.createPackageConfig.StepSelectConfig.filterAgentConfigsInputPlaceholder',
+ {
+ defaultMessage: 'Search for agent configurations',
+ }
+ ),
+ }}
+ height={180}
+ onChange={(options) => {
+ const selectedOption = options.find((option) => option.checked === 'on');
+ if (selectedOption) {
+ if (selectedOption.key !== selectedConfigId) {
+ setSelectedConfigId(selectedOption.key);
+ }
+ } else {
+ setSelectedConfigId(undefined);
+ }
+ }}
+ >
+ {(list, search) => (
+
+ {search}
+
+ {list}
+
+ )}
+
+
+ {/* Display selected agent config error if there is one */}
+ {selectedConfigError ? (
+
+
}
- ),
- }}
- height={240}
- onChange={(options) => {
- const selectedOption = options.find((option) => option.checked === 'on');
- if (selectedOption) {
- setSelectedConfigId(selectedOption.key);
- } else {
- setSelectedConfigId(undefined);
- }
- }}
- >
- {(list, search) => (
-
- {search}
-
- {list}
-
- )}
-
-
- {/* Display selected agent config error if there is one */}
- {selectedConfigError ? (
+ error={selectedConfigError}
+ />
+
+ ) : null}
-
+ setIsCreateAgentConfigFlyoutOpen(true)}
+ flush="left"
+ size="s"
+ >
- }
- error={selectedConfigError}
- />
+
+
- ) : null}
-
+
+ >
);
};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_package.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_package.tsx
index 204b862bd4dc4..048ae101fcd6f 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_package.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_package.tsx
@@ -22,7 +22,14 @@ export const StepSelectPackage: React.FunctionComponent<{
updateAgentConfig: (config: AgentConfig | undefined) => void;
packageInfo?: PackageInfo;
updatePackageInfo: (packageInfo: PackageInfo | undefined) => void;
-}> = ({ agentConfigId, updateAgentConfig, packageInfo, updatePackageInfo }) => {
+ setIsLoadingSecondStep: (isLoading: boolean) => void;
+}> = ({
+ agentConfigId,
+ updateAgentConfig,
+ packageInfo,
+ updatePackageInfo,
+ setIsLoadingSecondStep,
+}) => {
// Selected package state
const [selectedPkgKey, setSelectedPkgKey] = useState(
packageInfo ? `${packageInfo.name}-${packageInfo.version}` : undefined
@@ -30,7 +37,11 @@ export const StepSelectPackage: React.FunctionComponent<{
const [selectedPkgError, setSelectedPkgError] = useState();
// Fetch agent config info
- const { data: agentConfigData, error: agentConfigError } = useGetOneAgentConfig(agentConfigId);
+ const {
+ data: agentConfigData,
+ error: agentConfigError,
+ isLoading: isAgentConfigsLoading,
+ } = useGetOneAgentConfig(agentConfigId);
// Fetch packages info
// Filter out limited packages already part of selected agent config
@@ -66,6 +77,7 @@ export const StepSelectPackage: React.FunctionComponent<{
useEffect(() => {
const fetchPackageInfo = async () => {
if (selectedPkgKey) {
+ setIsLoadingSecondStep(true);
const { data, error } = await sendGetPackageInfoByKey(selectedPkgKey);
if (error) {
setSelectedPkgError(error);
@@ -74,6 +86,7 @@ export const StepSelectPackage: React.FunctionComponent<{
setSelectedPkgError(undefined);
updatePackageInfo(data.response);
}
+ setIsLoadingSecondStep(false);
} else {
setSelectedPkgError(undefined);
updatePackageInfo(undefined);
@@ -82,7 +95,7 @@ export const StepSelectPackage: React.FunctionComponent<{
if (!packageInfo || selectedPkgKey !== `${packageInfo.name}-${packageInfo.version}`) {
fetchPackageInfo();
}
- }, [selectedPkgKey, packageInfo, updatePackageInfo]);
+ }, [selectedPkgKey, packageInfo, updatePackageInfo, setIsLoadingSecondStep]);
// Display agent config error if there is one
if (agentConfigError) {
@@ -121,7 +134,7 @@ export const StepSelectPackage: React.FunctionComponent<{
searchable
allowExclusions={false}
singleSelection={true}
- isLoading={isPackagesLoading || isLimitedPackagesLoading}
+ isLoading={isPackagesLoading || isLimitedPackagesLoading || isAgentConfigsLoading}
options={packages.map(({ title, name, version, icons }) => {
const pkgkey = `${name}-${version}`;
return {
@@ -154,7 +167,9 @@ export const StepSelectPackage: React.FunctionComponent<{
onChange={(options) => {
const selectedOption = options.find((option) => option.checked === 'on');
if (selectedOption) {
- setSelectedPkgKey(selectedOption.key);
+ if (selectedOption.key !== selectedPkgKey) {
+ setSelectedPkgKey(selectedOption.key);
+ }
} else {
setSelectedPkgKey(undefined);
}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_package_config_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_package_config_page/index.tsx
index 52fd95d663671..f4411a6057a15 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_package_config_page/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_package_config_page/index.tsx
@@ -3,14 +3,13 @@
* 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 React, { useState, useEffect, useCallback, useMemo } 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,
@@ -160,38 +159,45 @@ export const EditPackageConfigPage: React.FunctionComponent = () => {
const [validationResults, setValidationResults] = useState();
const hasErrors = validationResults ? validationHasErrors(validationResults) : false;
- // Update package config method
- const updatePackageConfig = (updatedFields: Partial) => {
- const newPackageConfig = {
- ...packageConfig,
- ...updatedFields,
- };
- setPackageConfig(newPackageConfig);
+ // Update package config validation
+ const updatePackageConfigValidation = useCallback(
+ (newPackageConfig?: UpdatePackageConfig) => {
+ if (packageInfo) {
+ const newValidationResult = validatePackageConfig(
+ newPackageConfig || packageConfig,
+ packageInfo
+ );
+ setValidationResults(newValidationResult);
+ // eslint-disable-next-line no-console
+ console.debug('Package config validation results', newValidationResult);
- // eslint-disable-next-line no-console
- console.debug('Package config updated', newPackageConfig);
- const newValidationResults = updatePackageConfigValidation(newPackageConfig);
- const hasValidationErrors = newValidationResults
- ? validationHasErrors(newValidationResults)
- : false;
- if (!hasValidationErrors) {
- setFormState('VALID');
- }
- };
+ return newValidationResult;
+ }
+ },
+ [packageConfig, packageInfo]
+ );
- const updatePackageConfigValidation = (newPackageConfig?: UpdatePackageConfig) => {
- if (packageInfo) {
- const newValidationResult = validatePackageConfig(
- newPackageConfig || packageConfig,
- packageInfo
- );
- setValidationResults(newValidationResult);
- // eslint-disable-next-line no-console
- console.debug('Package config validation results', newValidationResult);
+ // Update package config method
+ const updatePackageConfig = useCallback(
+ (updatedFields: Partial) => {
+ const newPackageConfig = {
+ ...packageConfig,
+ ...updatedFields,
+ };
+ setPackageConfig(newPackageConfig);
- return newValidationResult;
- }
- };
+ // eslint-disable-next-line no-console
+ console.debug('Package config updated', newPackageConfig);
+ const newValidationResults = updatePackageConfigValidation(newPackageConfig);
+ const hasValidationErrors = newValidationResults
+ ? validationHasErrors(newValidationResults)
+ : false;
+ if (!hasValidationErrors) {
+ setFormState('VALID');
+ }
+ },
+ [packageConfig, updatePackageConfigValidation]
+ );
// Cancel url
const cancelUrl = getHref('configuration_details', { configId });
@@ -271,6 +277,40 @@ export const EditPackageConfigPage: React.FunctionComponent = () => {
packageInfo,
};
+ const configurePackage = useMemo(
+ () =>
+ agentConfig && packageInfo ? (
+ <>
+
+
+
+ >
+ ) : null,
+ [
+ agentConfig,
+ formState,
+ packageConfig,
+ packageConfigId,
+ packageInfo,
+ updatePackageConfig,
+ validationResults,
+ ]
+ );
+
return (
{isLoadingData ? (
@@ -301,46 +341,7 @@ export const EditPackageConfigPage: React.FunctionComponent = () => {
onCancel={() => setFormState('VALID')}
/>
)}
-
- ),
- },
- {
- title: i18n.translate(
- 'xpack.ingestManager.editPackageConfig.stepConfigurePackageConfigTitle',
- {
- defaultMessage: 'Select the data you want to collect',
- }
- ),
- children: (
-
- ),
- },
- ]}
- />
+ {configurePackage}
{/* TODO #64541 - Remove classes */}
{
: undefined
}
>
-
+
-
+ {agentConfig && packageInfo && formState === 'INVALID' ? (
-
+ ) : null}
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
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 d1abd88adba86..795c46ec282c5 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
@@ -18,12 +18,12 @@ import {
EuiButton,
EuiText,
} from '@elastic/eui';
-import { NewAgentConfig } from '../../../../types';
+import { NewAgentConfig, AgentConfig } from '../../../../types';
import { useCapabilities, useCore, sendCreateAgentConfig } from '../../../../hooks';
import { AgentConfigForm, agentConfigFormValidation } from '../../components';
interface Props {
- onClose: () => void;
+ onClose: (createdAgentConfig?: AgentConfig) => void;
}
export const CreateAgentConfigFlyout: React.FunctionComponent = ({ onClose }) => {
@@ -86,7 +86,7 @@ export const CreateAgentConfigFlyout: React.FunctionComponent = ({ onClos
-
+ onClose()} flush="left">
= ({ onClos
}
)
);
- onClose();
+ onClose(data.item);
} else {
notifications.toasts.addDanger(
error
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts
index 170a9cedc08d9..dc27da18bc008 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts
@@ -18,6 +18,7 @@ export {
UpdatePackageConfig,
PackageConfigInput,
PackageConfigInputStream,
+ PackageConfigConfigRecord,
PackageConfigConfigRecordEntry,
Output,
DataStream,
diff --git a/x-pack/plugins/ingest_manager/server/services/package_config.ts b/x-pack/plugins/ingest_manager/server/services/package_config.ts
index 9433a81e74b07..e8ca09a83c2b6 100644
--- a/x-pack/plugins/ingest_manager/server/services/package_config.ts
+++ b/x-pack/plugins/ingest_manager/server/services/package_config.ts
@@ -44,6 +44,20 @@ class PackageConfigService {
packageConfig: NewPackageConfig,
options?: { id?: string; user?: AuthenticatedUser }
): Promise {
+ // Check that its agent config does not have a package config with the same name
+ const parentAgentConfig = await agentConfigService.get(soClient, packageConfig.config_id);
+ if (!parentAgentConfig) {
+ throw new Error('Agent config not found');
+ } else {
+ if (
+ (parentAgentConfig.package_configs as PackageConfig[]).find(
+ (siblingPackageConfig) => siblingPackageConfig.name === packageConfig.name
+ )
+ ) {
+ throw new Error('There is already a package with the same name on this agent config');
+ }
+ }
+
// Make sure the associated package is installed
if (packageConfig.package?.name) {
const [, pkgInfo] = await Promise.all([
@@ -225,6 +239,21 @@ class PackageConfigService {
throw new Error('Package config not found');
}
+ // Check that its agent config does not have a package config with the same name
+ const parentAgentConfig = await agentConfigService.get(soClient, packageConfig.config_id);
+ if (!parentAgentConfig) {
+ throw new Error('Agent config not found');
+ } else {
+ if (
+ (parentAgentConfig.package_configs as PackageConfig[]).find(
+ (siblingPackageConfig) =>
+ siblingPackageConfig.id !== id && siblingPackageConfig.name === packageConfig.name
+ )
+ ) {
+ throw new Error('There is already a package with the same name on this agent config');
+ }
+ }
+
await soClient.update(
SAVED_OBJECT_TYPE,
id,
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 4118053396e90..97afa9e058b98 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -8173,7 +8173,6 @@
"xpack.ingestManager.createPackageConfig.agentConfigurationNameLabel": "構成",
"xpack.ingestManager.createPackageConfig.cancelButton": "キャンセル",
"xpack.ingestManager.createPackageConfig.cancelLinkText": "キャンセル",
- "xpack.ingestManager.createPackageConfig.packageNameLabel": "統合",
"xpack.ingestManager.createPackageConfig.pageDescriptionfromConfig": "次の手順に従い、統合をこのエージェント構成に追加します。",
"xpack.ingestManager.createPackageConfig.pageDescriptionfromPackage": "次の手順に従い、この統合をエージェント構成に追加します。",
"xpack.ingestManager.createPackageConfig.pageTitle": "データソースを追加",
@@ -8184,19 +8183,12 @@
"xpack.ingestManager.createPackageConfig.stepConfigure.packageConfigNameInputLabel": "データソース名",
"xpack.ingestManager.createPackageConfig.stepConfigure.packageConfigNamespaceInputLabel": "名前空間",
"xpack.ingestManager.createPackageConfig.stepConfigure.hideStreamsAriaLabel": "{type} ストリームを隠す",
- "xpack.ingestManager.createPackageConfig.stepConfigure.inputConfigErrorsTooltip": "構成エラーを修正してください",
- "xpack.ingestManager.createPackageConfig.stepConfigure.inputLevelErrorsTooltip": "構成エラーを修正してください",
"xpack.ingestManager.createPackageConfig.stepConfigure.inputSettingsDescription": "次の設定はすべてのストリームに適用されます。",
"xpack.ingestManager.createPackageConfig.stepConfigure.inputSettingsTitle": "設定",
"xpack.ingestManager.createPackageConfig.stepConfigure.inputVarFieldOptionalLabel": "オプション",
"xpack.ingestManager.createPackageConfig.stepConfigure.noConfigOptionsMessage": "構成するものがありません",
"xpack.ingestManager.createPackageConfig.stepConfigure.showStreamsAriaLabel": "{type} ストリームを表示",
- "xpack.ingestManager.createPackageConfig.stepConfigure.streamLevelErrorsTooltip": "構成エラーを修正してください",
- "xpack.ingestManager.createPackageConfig.stepConfigure.streamsEnabledCountText": "{count} / {total, plural, one {# ストリーム} other {# ストリーム}}が有効です",
"xpack.ingestManager.createPackageConfig.stepConfigure.toggleAdvancedOptionsButtonText": "高度なオプション",
- "xpack.ingestManager.createPackageConfig.stepConfigure.validationErrorText": "続行する前に、上記のエラーを修正してください",
- "xpack.ingestManager.createPackageConfig.stepConfigure.validationErrorTitle": "データソース構成にエラーがあります",
- "xpack.ingestManager.createPackageConfig.stepDefinePackageConfigTitle": "データソースを定義",
"xpack.ingestManager.createPackageConfig.stepSelectAgentConfigTitle": "エージェント構成を選択する",
"xpack.ingestManager.createPackageConfig.StepSelectConfig.agentConfigAgentsCountText": "{count, plural, one {# エージェント} other {# エージェント}}",
"xpack.ingestManager.createPackageConfig.StepSelectConfig.errorLoadingAgentConfigsTitle": "エージェント構成の読み込みエラー",
@@ -8268,8 +8260,6 @@
"xpack.ingestManager.editPackageConfig.pageDescription": "次の手順に従い、このデータソースを編集します。",
"xpack.ingestManager.editPackageConfig.pageTitle": "データソースを編集",
"xpack.ingestManager.editPackageConfig.saveButton": "データソースを保存",
- "xpack.ingestManager.editPackageConfig.stepConfigurePackageConfigTitle": "収集するデータを選択",
- "xpack.ingestManager.editPackageConfig.stepDefinePackageConfigTitle": "データソースを定義",
"xpack.ingestManager.editPackageConfig.updatedNotificationMessage": "フリートは'{agentConfigName}'構成で使用されているすべてのエージェントに更新をデプロイします。",
"xpack.ingestManager.editPackageConfig.updatedNotificationTitle": "正常に'{packageConfigName}'を更新しました",
"xpack.ingestManager.enrollemntAPIKeyList.emptyMessage": "登録トークンが見つかりません。",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 01939bea417d4..31a33bfa09b64 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -8178,7 +8178,6 @@
"xpack.ingestManager.createPackageConfig.agentConfigurationNameLabel": "配置",
"xpack.ingestManager.createPackageConfig.cancelButton": "取消",
"xpack.ingestManager.createPackageConfig.cancelLinkText": "取消",
- "xpack.ingestManager.createPackageConfig.packageNameLabel": "集成",
"xpack.ingestManager.createPackageConfig.pageDescriptionfromConfig": "按照下面的说明将集成添加此代理配置。",
"xpack.ingestManager.createPackageConfig.pageDescriptionfromPackage": "按照下面的说明将此集成添加代理配置。",
"xpack.ingestManager.createPackageConfig.pageTitle": "添加数据源",
@@ -8189,19 +8188,12 @@
"xpack.ingestManager.createPackageConfig.stepConfigure.packageConfigNameInputLabel": "数据源名称",
"xpack.ingestManager.createPackageConfig.stepConfigure.packageConfigNamespaceInputLabel": "命名空间",
"xpack.ingestManager.createPackageConfig.stepConfigure.hideStreamsAriaLabel": "隐藏 {type} 流",
- "xpack.ingestManager.createPackageConfig.stepConfigure.inputConfigErrorsTooltip": "解决配置错误",
- "xpack.ingestManager.createPackageConfig.stepConfigure.inputLevelErrorsTooltip": "解决配置错误",
"xpack.ingestManager.createPackageConfig.stepConfigure.inputSettingsDescription": "以下设置适用于所有流。",
"xpack.ingestManager.createPackageConfig.stepConfigure.inputSettingsTitle": "设置",
"xpack.ingestManager.createPackageConfig.stepConfigure.inputVarFieldOptionalLabel": "可选",
"xpack.ingestManager.createPackageConfig.stepConfigure.noConfigOptionsMessage": "没有可配置的内容",
"xpack.ingestManager.createPackageConfig.stepConfigure.showStreamsAriaLabel": "显示 {type} 流",
- "xpack.ingestManager.createPackageConfig.stepConfigure.streamLevelErrorsTooltip": "解决配置错误",
- "xpack.ingestManager.createPackageConfig.stepConfigure.streamsEnabledCountText": "{count} / {total, plural, one {# 个流} other {# 个流}}已启用",
"xpack.ingestManager.createPackageConfig.stepConfigure.toggleAdvancedOptionsButtonText": "高级选项",
- "xpack.ingestManager.createPackageConfig.stepConfigure.validationErrorText": "在继续之前请解决上述错误",
- "xpack.ingestManager.createPackageConfig.stepConfigure.validationErrorTitle": "您的数据源配置有错误",
- "xpack.ingestManager.createPackageConfig.stepDefinePackageConfigTitle": "定义您的数据源",
"xpack.ingestManager.createPackageConfig.stepSelectAgentConfigTitle": "选择代理配置",
"xpack.ingestManager.createPackageConfig.StepSelectConfig.agentConfigAgentsCountText": "{count, plural, one {# 个代理} other {# 个代理}}",
"xpack.ingestManager.createPackageConfig.StepSelectConfig.errorLoadingAgentConfigsTitle": "加载代理配置时出错",
@@ -8273,8 +8265,6 @@
"xpack.ingestManager.editPackageConfig.pageDescription": "按照下面的说明编辑此数据源。",
"xpack.ingestManager.editPackageConfig.pageTitle": "编辑数据源",
"xpack.ingestManager.editPackageConfig.saveButton": "保存数据源",
- "xpack.ingestManager.editPackageConfig.stepConfigurePackageConfigTitle": "选择要收集的数据",
- "xpack.ingestManager.editPackageConfig.stepDefinePackageConfigTitle": "定义您的数据源",
"xpack.ingestManager.editPackageConfig.updatedNotificationMessage": "Fleet 会将更新部署到所有使用配置“{agentConfigName}”的代理",
"xpack.ingestManager.editPackageConfig.updatedNotificationTitle": "已成功更新“{packageConfigName}”",
"xpack.ingestManager.enrollemntAPIKeyList.emptyMessage": "未找到任何注册令牌。",
diff --git a/x-pack/test/ingest_manager_api_integration/apis/index.js b/x-pack/test/ingest_manager_api_integration/apis/index.js
index 30c49140c6e2a..81848917f9b05 100644
--- a/x-pack/test/ingest_manager_api_integration/apis/index.js
+++ b/x-pack/test/ingest_manager_api_integration/apis/index.js
@@ -17,5 +17,6 @@ export default function ({ loadTestFile }) {
// Package configs
loadTestFile(require.resolve('./package_config/create'));
+ loadTestFile(require.resolve('./package_config/update'));
});
}
diff --git a/x-pack/test/ingest_manager_api_integration/apis/package_config/create.ts b/x-pack/test/ingest_manager_api_integration/apis/package_config/create.ts
index c7748ab255f43..cae4ff79bdef6 100644
--- a/x-pack/test/ingest_manager_api_integration/apis/package_config/create.ts
+++ b/x-pack/test/ingest_manager_api_integration/apis/package_config/create.ts
@@ -126,5 +126,48 @@ export default function ({ getService }: FtrProviderContext) {
warnAndSkipTest(this, log);
}
});
+
+ it('should return a 500 if there is another package config with the same name', async function () {
+ if (server.enabled) {
+ await supertest
+ .post(`/api/ingest_manager/package_configs`)
+ .set('kbn-xsrf', 'xxxx')
+ .send({
+ name: 'same-name-test-1',
+ description: '',
+ namespace: 'default',
+ config_id: agentConfigId,
+ enabled: true,
+ output_id: '',
+ inputs: [],
+ package: {
+ name: 'filetest',
+ title: 'For File Tests',
+ version: '0.1.0',
+ },
+ })
+ .expect(200);
+ await supertest
+ .post(`/api/ingest_manager/package_configs`)
+ .set('kbn-xsrf', 'xxxx')
+ .send({
+ name: 'same-name-test-1',
+ description: '',
+ namespace: 'default',
+ config_id: agentConfigId,
+ enabled: true,
+ output_id: '',
+ inputs: [],
+ package: {
+ name: 'filetest',
+ title: 'For File Tests',
+ version: '0.1.0',
+ },
+ })
+ .expect(500);
+ } else {
+ warnAndSkipTest(this, log);
+ }
+ });
});
}
diff --git a/x-pack/test/ingest_manager_api_integration/apis/package_config/update.ts b/x-pack/test/ingest_manager_api_integration/apis/package_config/update.ts
new file mode 100644
index 0000000000000..0251fef5f767c
--- /dev/null
+++ b/x-pack/test/ingest_manager_api_integration/apis/package_config/update.ts
@@ -0,0 +1,127 @@
+/*
+ * 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 expect from '@kbn/expect';
+import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
+import { warnAndSkipTest } from '../../helpers';
+
+export default function ({ getService }: FtrProviderContext) {
+ const log = getService('log');
+ const supertest = getService('supertest');
+ const dockerServers = getService('dockerServers');
+
+ const server = dockerServers.get('registry');
+ // use function () {} and not () => {} here
+ // because `this` has to point to the Mocha context
+ // see https://mochajs.org/#arrow-functions
+
+ describe('Package Config - update', async function () {
+ let agentConfigId: string;
+ let packageConfigId: string;
+ let packageConfigId2: string;
+
+ before(async function () {
+ const { body: agentConfigResponse } = await supertest
+ .post(`/api/ingest_manager/agent_configs`)
+ .set('kbn-xsrf', 'xxxx')
+ .send({
+ name: 'Test config',
+ namespace: 'default',
+ });
+ agentConfigId = agentConfigResponse.item.id;
+
+ const { body: packageConfigResponse } = await supertest
+ .post(`/api/ingest_manager/package_configs`)
+ .set('kbn-xsrf', 'xxxx')
+ .send({
+ name: 'filetest-1',
+ description: '',
+ namespace: 'default',
+ config_id: agentConfigId,
+ enabled: true,
+ output_id: '',
+ inputs: [],
+ package: {
+ name: 'filetest',
+ title: 'For File Tests',
+ version: '0.1.0',
+ },
+ });
+ packageConfigId = packageConfigResponse.item.id;
+
+ const { body: packageConfigResponse2 } = await supertest
+ .post(`/api/ingest_manager/package_configs`)
+ .set('kbn-xsrf', 'xxxx')
+ .send({
+ name: 'filetest-2',
+ description: '',
+ namespace: 'default',
+ config_id: agentConfigId,
+ enabled: true,
+ output_id: '',
+ inputs: [],
+ package: {
+ name: 'filetest',
+ title: 'For File Tests',
+ version: '0.1.0',
+ },
+ });
+ packageConfigId2 = packageConfigResponse2.item.id;
+ });
+
+ it('should work with valid values', async function () {
+ if (server.enabled) {
+ const { body: apiResponse } = await supertest
+ .put(`/api/ingest_manager/package_configs/${packageConfigId}`)
+ .set('kbn-xsrf', 'xxxx')
+ .send({
+ name: 'filetest-1',
+ description: '',
+ namespace: 'updated_namespace',
+ config_id: agentConfigId,
+ enabled: true,
+ output_id: '',
+ inputs: [],
+ package: {
+ name: 'filetest',
+ title: 'For File Tests',
+ version: '0.1.0',
+ },
+ })
+ .expect(200);
+
+ expect(apiResponse.success).to.be(true);
+ } else {
+ warnAndSkipTest(this, log);
+ }
+ });
+
+ it('should return a 500 if there is another package config with the same name', async function () {
+ if (server.enabled) {
+ await supertest
+ .put(`/api/ingest_manager/package_configs/${packageConfigId2}`)
+ .set('kbn-xsrf', 'xxxx')
+ .send({
+ name: 'filetest-1',
+ description: '',
+ namespace: 'updated_namespace',
+ config_id: agentConfigId,
+ enabled: true,
+ output_id: '',
+ inputs: [],
+ package: {
+ name: 'filetest',
+ title: 'For File Tests',
+ version: '0.1.0',
+ },
+ })
+ .expect(500);
+ } else {
+ warnAndSkipTest(this, log);
+ }
+ });
+ });
+}
From 8a4d0d06a5edcc8d3679a3a04964c87ce1a7cd27 Mon Sep 17 00:00:00 2001
From: spalger
Date: Mon, 13 Jul 2020 08:16:26 -0700
Subject: [PATCH 07/40] add old .chromium to gitignore to prevent it from being
accidentally committed
---
x-pack/.gitignore | 1 +
1 file changed, 1 insertion(+)
diff --git a/x-pack/.gitignore b/x-pack/.gitignore
index e181caf2b1a49..0c916ef0e9b91 100644
--- a/x-pack/.gitignore
+++ b/x-pack/.gitignore
@@ -6,6 +6,7 @@
/test/page_load_metrics/screenshots
/test/functional/apps/reporting/reports/session
/test/reporting/configs/failure_debug/
+/plugins/reporting/.chromium/
/legacy/plugins/reporting/.chromium/
/legacy/plugins/reporting/.phantom/
/plugins/reporting/chromium/
From 327fed87bbb369b3061de3862a1ceefcbfa1e5ee Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?=
Date: Mon, 13 Jul 2020 17:21:22 +0200
Subject: [PATCH 08/40] [APM] Improvements to the ML Settings page (#71309)
---
.../Settings/anomaly_detection/add_environments.tsx | 8 +++++++-
.../app/Settings/anomaly_detection/index.tsx | 8 ++++++--
.../app/Settings/anomaly_detection/jobs_list.tsx | 12 +++++++-----
.../anomaly_detection/get_anomaly_detection_jobs.ts | 2 +-
...obs_by_group.ts => get_ml_jobs_with_apm_group.ts} | 0
.../server/lib/anomaly_detection/has_legacy_jobs.ts | 2 +-
.../apm/server/routes/settings/anomaly_detection.ts | 7 +++++--
x-pack/plugins/apm/typings/anomaly_detection.ts | 10 ----------
8 files changed, 27 insertions(+), 22 deletions(-)
rename x-pack/plugins/apm/server/lib/anomaly_detection/{get_ml_jobs_by_group.ts => get_ml_jobs_with_apm_group.ts} (100%)
delete mode 100644 x-pack/plugins/apm/typings/anomaly_detection.ts
diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx
index 98b4ae2f4b63f..4c056d48f4b14 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx
@@ -50,6 +50,8 @@ export const AddEnvironments = ({
disabled: currentEnvironments.includes(env),
}));
+ const [isSaving, setIsSaving] = useState(false);
+
const [selectedOptions, setSelected] = useState<
Array>
>([]);
@@ -127,9 +129,12 @@ export const AddEnvironments = ({
{
+ setIsSaving(true);
+
const selectedEnvironments = selectedOptions.map(
({ value }) => value as string
);
@@ -140,6 +145,7 @@ export const AddEnvironments = ({
if (success) {
onCreateJobSuccess();
}
+ setIsSaving(false);
}}
>
{i18n.translate(
diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx
index 6f985d06dba9d..f02350fafbabb 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx
@@ -15,7 +15,11 @@ import { LicensePrompt } from '../../../shared/LicensePrompt';
import { useLicense } from '../../../../hooks/useLicense';
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
-const DEFAULT_VALUE: APIReturnType<'/api/apm/settings/anomaly-detection'> = {
+export type AnomalyDetectionApiResponse = APIReturnType<
+ '/api/apm/settings/anomaly-detection'
+>;
+
+const DEFAULT_VALUE: AnomalyDetectionApiResponse = {
jobs: [],
hasLegacyJobs: false,
};
@@ -80,7 +84,7 @@ export const AnomalyDetection = () => {
) : (
{
setViewAddEnvironments(true);
diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx
index 34687e5a8094e..5954b82f3b9e7 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx
@@ -19,13 +19,15 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { FETCH_STATUS } from '../../../../hooks/useFetcher';
import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable';
import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
-import { AnomalyDetectionJobByEnv } from '../../../../../typings/anomaly_detection';
import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink';
import { MLLink } from '../../../shared/Links/MachineLearningLinks/MLLink';
import { getEnvironmentLabel } from '../../../../../common/environment_filter_values';
import { LegacyJobsCallout } from './legacy_jobs_callout';
+import { AnomalyDetectionApiResponse } from './index';
-const columns: Array> = [
+type Jobs = AnomalyDetectionApiResponse['jobs'];
+
+const columns: Array> = [
{
field: 'environment',
name: i18n.translate(
@@ -57,13 +59,13 @@ const columns: Array> = [
interface Props {
status: FETCH_STATUS;
onAddEnvironments: () => void;
- anomalyDetectionJobsByEnv: AnomalyDetectionJobByEnv[];
+ jobs: Jobs;
hasLegacyJobs: boolean;
}
export const JobsList = ({
status,
onAddEnvironments,
- anomalyDetectionJobsByEnv,
+ jobs,
hasLegacyJobs,
}: Props) => {
const isLoading =
@@ -128,7 +130,7 @@ export const JobsList = ({
)
}
columns={columns}
- items={isLoading || hasFetchFailure ? [] : anomalyDetectionJobsByEnv}
+ items={jobs}
/>
diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts
index 8fdebeb597eaf..13b30f159eed1 100644
--- a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts
+++ b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts
@@ -6,7 +6,7 @@
import { Logger } from 'kibana/server';
import { Setup } from '../helpers/setup_request';
-import { getMlJobsWithAPMGroup } from './get_ml_jobs_by_group';
+import { getMlJobsWithAPMGroup } from './get_ml_jobs_with_apm_group';
export async function getAnomalyDetectionJobs(setup: Setup, logger: Logger) {
const { ml } = setup;
diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_by_group.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_with_apm_group.ts
similarity index 100%
rename from x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_by_group.ts
rename to x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_with_apm_group.ts
diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts
index bf502607fcc1d..999d28309121a 100644
--- a/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts
+++ b/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Setup } from '../helpers/setup_request';
-import { getMlJobsWithAPMGroup } from './get_ml_jobs_by_group';
+import { getMlJobsWithAPMGroup } from './get_ml_jobs_with_apm_group';
// Determine whether there are any legacy ml jobs.
// A legacy ML job has a job id that ends with "high_mean_response_time" and created_by=ml-module-apm-transaction
diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts
index 7009470e1ff17..4d564b773e397 100644
--- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts
+++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts
@@ -18,10 +18,13 @@ export const anomalyDetectionJobsRoute = createRoute(() => ({
path: '/api/apm/settings/anomaly-detection',
handler: async ({ context, request }) => {
const setup = await setupRequest(context, request);
- const jobs = await getAnomalyDetectionJobs(setup, context.logger);
+ const [jobs, legacyJobs] = await Promise.all([
+ getAnomalyDetectionJobs(setup, context.logger),
+ hasLegacyJobs(setup),
+ ]);
return {
jobs,
- hasLegacyJobs: await hasLegacyJobs(setup),
+ hasLegacyJobs: legacyJobs,
};
},
}));
diff --git a/x-pack/plugins/apm/typings/anomaly_detection.ts b/x-pack/plugins/apm/typings/anomaly_detection.ts
deleted file mode 100644
index 30dc92c36dea4..0000000000000
--- a/x-pack/plugins/apm/typings/anomaly_detection.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-export interface AnomalyDetectionJobByEnv {
- environment: string;
- job_id: string;
-}
From 24edc804c94a0df8a7a8ddece377978882d5c364 Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Mon, 13 Jul 2020 16:30:03 +0100
Subject: [PATCH 09/40] Node options from cfg file for production (#62468)
* chore(NA): load NODE_OPTIONS from options files across environments
* chore(NA): move node.ci.options to config folder
* docs(NA): update docs to explain how to set node options from the cfg fil
* chore(NA): removed test npm scripts
* fix(NA): typo on setup script for CI
* chore(NA): add debug info
* chore(NA): export options on CI
* chore(NA): remove debug info
* chore(NA): support for configurable config folder using env var
* chore(NA): add node.options file into docker img
* fix(NA): use calculated config dir on node options for ci
* chore(NA): node bin scripts bootstrap and node_with_options implementation for bash
* chore(NA): complete node_with_options scripts with bat version
* chore(NA): add bin/node dev script and remove cli for run_with_node_options
* chore(NA): increase default maxBuffer
* chore(NA): remove run with options script from package.json
* chore(NA): include kbn-node script and underlying usage of it
* chore(NA): remove change on eslint
* chore(NA): correct typo on kbn node script comment
Co-authored-by: Tyler Smalley
* chore(NA): correct typo on kbn node script comment
Co-authored-by: Tyler Smalley
* chore(NA): add line to describe each option should be specified in a separated line
* chore(NA): remove node options from dev and ci env
* chore(NA): remove changes from package.json
* chore(NA): fix docker image build
* chore(NA): change value for example of --max-old-space-size in the node.options file
Co-authored-by: Tyler Smalley
* chore(NA): remove --no-warnings from node.options and force it in the bin scripts
* chore(NA): prevent 'The system cannot find the file' error message
* chore(NA): introduce slash when building path for %DIR%
* chore(NA): read options from file only if it exists
Co-authored-by: Jonathan Budzenski
Co-authored-by: Elastic Machine
Co-authored-by: Tyler Smalley
---
.gitignore | 1 +
config/node.options | 6 +++++
docs/setup/production.asciidoc | 4 ++--
package.json | 2 +-
src/dev/build/tasks/bin/scripts/kibana | 7 +++++-
.../build/tasks/bin/scripts/kibana-keystore | 7 +++++-
.../tasks/bin/scripts/kibana-keystore.bat | 17 ++++++++++++-
src/dev/build/tasks/bin/scripts/kibana-plugin | 7 +++++-
.../build/tasks/bin/scripts/kibana-plugin.bat | 23 +++++++++++++++---
src/dev/build/tasks/bin/scripts/kibana.bat | 24 +++++++++++++++++--
src/dev/build/tasks/copy_source_task.js | 1 +
11 files changed, 87 insertions(+), 12 deletions(-)
create mode 100644 config/node.options
diff --git a/.gitignore b/.gitignore
index 716cea937f9c0..dfd02de7b1186 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,6 +31,7 @@ disabledPlugins
webpackstats.json
/config/*
!/config/kibana.yml
+!/config/node.options
coverage
selenium
.babel_register_cache.json
diff --git a/config/node.options b/config/node.options
new file mode 100644
index 0000000000000..2927d1b576716
--- /dev/null
+++ b/config/node.options
@@ -0,0 +1,6 @@
+## Node command line options
+## See `node --help` and `node --v8-options` for available options
+## Please note you should specify one option per line
+
+## max size of old space in megabytes
+#--max-old-space-size=4096
diff --git a/docs/setup/production.asciidoc b/docs/setup/production.asciidoc
index 72f275e237490..afb4b37df6a28 100644
--- a/docs/setup/production.asciidoc
+++ b/docs/setup/production.asciidoc
@@ -167,9 +167,9 @@ These can be used to automatically update the list of hosts as a cluster is resi
Kibana has a default maximum memory limit of 1.4 GB, and in most cases, we recommend leaving this unconfigured. In some scenarios, such as large reporting jobs,
it may make sense to tweak limits to meet more specific requirements.
-You can modify this limit by setting `--max-old-space-size` in the `NODE_OPTIONS` environment variable. For deb and rpm, packages this is passed in via `/etc/default/kibana` and can be appended to the bottom of the file.
+You can modify this limit by setting `--max-old-space-size` in the `node.options` config file that can be found inside `kibana/config` folder or any other configured with the environment variable `KIBANA_PATH_CONF` (for example in debian based system would be `/etc/kibana`).
The option accepts a limit in MB:
--------
-NODE_OPTIONS="--max-old-space-size=2048" bin/kibana
+--max-old-space-size=2048
--------
diff --git a/package.json b/package.json
index 7889909b15244..7ab6bfb91a376 100644
--- a/package.json
+++ b/package.json
@@ -67,7 +67,7 @@
"uiFramework:documentComponent": "cd packages/kbn-ui-framework && yarn documentComponent",
"kbn:watch": "node scripts/kibana --dev --logging.json=false",
"build:types": "tsc --p tsconfig.types.json",
- "docs:acceptApiChanges": "node --max-old-space-size=6144 scripts/check_published_api_changes.js --accept",
+ "docs:acceptApiChanges": "node --max-old-space-size=6144 scripts/check_published_api_changes.js --accept",
"kbn:bootstrap": "node scripts/register_git_hook",
"spec_to_console": "node scripts/spec_to_console",
"backport-skip-ci": "backport --prDescription \"[skip-ci]\"",
diff --git a/src/dev/build/tasks/bin/scripts/kibana b/src/dev/build/tasks/bin/scripts/kibana
index 558facb9da32b..3283e17008e7c 100755
--- a/src/dev/build/tasks/bin/scripts/kibana
+++ b/src/dev/build/tasks/bin/scripts/kibana
@@ -14,6 +14,7 @@ while [ -h "$SCRIPT" ] ; do
done
DIR="$(dirname "${SCRIPT}")/.."
+CONFIG_DIR=${KIBANA_PATH_CONF:-"$DIR/config"}
NODE="${DIR}/node/bin/node"
test -x "$NODE"
if [ ! -x "$NODE" ]; then
@@ -21,4 +22,8 @@ if [ ! -x "$NODE" ]; then
exit 1
fi
-NODE_OPTIONS="--no-warnings --max-http-header-size=65536 ${NODE_OPTIONS}" NODE_ENV=production exec "${NODE}" "${DIR}/src/cli" ${@}
+if [ -f "${CONFIG_DIR}/node.options" ]; then
+ KBN_NODE_OPTS="$(grep -v ^# < ${CONFIG_DIR}/node.options | xargs)"
+fi
+
+NODE_OPTIONS="--no-warnings --max-http-header-size=65536 $KBN_NODE_OPTS $NODE_OPTIONS" NODE_ENV=production exec "${NODE}" "${DIR}/src/cli" ${@}
diff --git a/src/dev/build/tasks/bin/scripts/kibana-keystore b/src/dev/build/tasks/bin/scripts/kibana-keystore
index 43800c7b895d3..f83df118d24e8 100755
--- a/src/dev/build/tasks/bin/scripts/kibana-keystore
+++ b/src/dev/build/tasks/bin/scripts/kibana-keystore
@@ -14,6 +14,7 @@ while [ -h "$SCRIPT" ] ; do
done
DIR="$(dirname "${SCRIPT}")/.."
+CONFIG_DIR=${KIBANA_PATH_CONF:-"$DIR/config"}
NODE="${DIR}/node/bin/node"
test -x "$NODE"
if [ ! -x "$NODE" ]; then
@@ -21,4 +22,8 @@ if [ ! -x "$NODE" ]; then
exit 1
fi
-"${NODE}" "${DIR}/src/cli_keystore" "$@"
+if [ -f "${CONFIG_DIR}/node.options" ]; then
+ KBN_NODE_OPTS="$(grep -v ^# < ${CONFIG_DIR}/node.options | xargs)"
+fi
+
+NODE_OPTIONS="$KBN_NODE_OPTS $NODE_OPTIONS" "${NODE}" "${DIR}/src/cli_keystore" "$@"
diff --git a/src/dev/build/tasks/bin/scripts/kibana-keystore.bat b/src/dev/build/tasks/bin/scripts/kibana-keystore.bat
index b8311db2cfae5..389eb5bf488e4 100644
--- a/src/dev/build/tasks/bin/scripts/kibana-keystore.bat
+++ b/src/dev/build/tasks/bin/scripts/kibana-keystore.bat
@@ -1,6 +1,6 @@
@echo off
-SETLOCAL
+SETLOCAL ENABLEDELAYEDEXPANSION
set SCRIPT_DIR=%~dp0
for %%I in ("%SCRIPT_DIR%..") do set DIR=%%~dpfI
@@ -12,6 +12,21 @@ If Not Exist "%NODE%" (
Exit /B 1
)
+set CONFIG_DIR=%KIBANA_PATH_CONF%
+If [%KIBANA_PATH_CONF%] == [] (
+ set CONFIG_DIR=%DIR%\config
+)
+
+IF EXIST "%CONFIG_DIR%\node.options" (
+ for /F "eol=# tokens=*" %%i in (%CONFIG_DIR%\node.options) do (
+ If [!NODE_OPTIONS!] == [] (
+ set "NODE_OPTIONS=%%i"
+ ) Else (
+ set "NODE_OPTIONS=!NODE_OPTIONS! %%i"
+ )
+ )
+)
+
TITLE Kibana Keystore
"%NODE%" "%DIR%\src\cli_keystore" %*
diff --git a/src/dev/build/tasks/bin/scripts/kibana-plugin b/src/dev/build/tasks/bin/scripts/kibana-plugin
index b843d4966c6d1..f1102e1ef5a32 100755
--- a/src/dev/build/tasks/bin/scripts/kibana-plugin
+++ b/src/dev/build/tasks/bin/scripts/kibana-plugin
@@ -14,6 +14,7 @@ while [ -h "$SCRIPT" ] ; do
done
DIR="$(dirname "${SCRIPT}")/.."
+CONFIG_DIR=${KIBANA_PATH_CONF:-"$DIR/config"}
NODE="${DIR}/node/bin/node"
test -x "$NODE"
if [ ! -x "$NODE" ]; then
@@ -21,4 +22,8 @@ if [ ! -x "$NODE" ]; then
exit 1
fi
-NODE_OPTIONS="--no-warnings ${NODE_OPTIONS}" NODE_ENV=production exec "${NODE}" "${DIR}/src/cli_plugin" "$@"
+if [ -f "${CONFIG_DIR}/node.options" ]; then
+ KBN_NODE_OPTS="$(grep -v ^# < ${CONFIG_DIR}/node.options | xargs)"
+fi
+
+NODE_OPTIONS="--no-warnings $KBN_NODE_OPTS $NODE_OPTIONS" NODE_ENV=production exec "${NODE}" "${DIR}/src/cli_plugin" "$@"
diff --git a/src/dev/build/tasks/bin/scripts/kibana-plugin.bat b/src/dev/build/tasks/bin/scripts/kibana-plugin.bat
index bf382a0657ade..6815b1b9eab8c 100755
--- a/src/dev/build/tasks/bin/scripts/kibana-plugin.bat
+++ b/src/dev/build/tasks/bin/scripts/kibana-plugin.bat
@@ -1,6 +1,6 @@
@echo off
-SETLOCAL
+SETLOCAL ENABLEDELAYEDEXPANSION
set SCRIPT_DIR=%~dp0
for %%I in ("%SCRIPT_DIR%..") do set DIR=%%~dpfI
@@ -13,9 +13,26 @@ If Not Exist "%NODE%" (
Exit /B 1
)
-TITLE Kibana Server
+set CONFIG_DIR=%KIBANA_PATH_CONF%
+If [%KIBANA_PATH_CONF%] == [] (
+ set CONFIG_DIR=%DIR%\config
+)
+
+IF EXIST "%CONFIG_DIR%\node.options" (
+ for /F "eol=# tokens=*" %%i in (%CONFIG_DIR%\node.options) do (
+ If [!NODE_OPTIONS!] == [] (
+ set "NODE_OPTIONS=%%i"
+ ) Else (
+ set "NODE_OPTIONS=!NODE_OPTIONS! %%i"
+ )
+ )
+)
+
+:: Include pre-defined node option
+set "NODE_OPTIONS=--no-warnings %NODE_OPTIONS%"
-set "NODE_OPTIONS=--no-warnings %NODE_OPTIONS%" && "%NODE%" "%DIR%\src\cli_plugin" %*
+TITLE Kibana Server
+"%NODE%" "%DIR%\src\cli_plugin" %*
:finally
diff --git a/src/dev/build/tasks/bin/scripts/kibana.bat b/src/dev/build/tasks/bin/scripts/kibana.bat
index 9d8ba359e53af..d3edc92f110a5 100755
--- a/src/dev/build/tasks/bin/scripts/kibana.bat
+++ b/src/dev/build/tasks/bin/scripts/kibana.bat
@@ -1,6 +1,6 @@
@echo off
-SETLOCAL
+SETLOCAL ENABLEDELAYEDEXPANSION
set SCRIPT_DIR=%~dp0
for %%I in ("%SCRIPT_DIR%..") do set DIR=%%~dpfI
@@ -14,7 +14,27 @@ If Not Exist "%NODE%" (
Exit /B 1
)
-set "NODE_OPTIONS=--no-warnings --max-http-header-size=65536 %NODE_OPTIONS%" && "%NODE%" "%DIR%\src\cli" %*
+set CONFIG_DIR=%KIBANA_PATH_CONF%
+If [%KIBANA_PATH_CONF%] == [] (
+ set CONFIG_DIR=%DIR%\config
+)
+
+IF EXIST "%CONFIG_DIR%\node.options" (
+ for /F "eol=# tokens=*" %%i in (%CONFIG_DIR%\node.options) do (
+ If [!NODE_OPTIONS!] == [] (
+ set "NODE_OPTIONS=%%i"
+ ) Else (
+ set "NODE_OPTIONS=!NODE_OPTIONS! %%i"
+ )
+ )
+)
+
+:: Include pre-defined node option
+set "NODE_OPTIONS=--no-warnings --max-http-header-size=65536 %NODE_OPTIONS%"
+
+:: This should run independently as the last instruction
+:: as we need NODE_OPTIONS previously set to expand
+"%NODE%" "%DIR%\src\cli" %*
:finally
diff --git a/src/dev/build/tasks/copy_source_task.js b/src/dev/build/tasks/copy_source_task.js
index 32eb7bf8712e3..e34f05bd6cfff 100644
--- a/src/dev/build/tasks/copy_source_task.js
+++ b/src/dev/build/tasks/copy_source_task.js
@@ -43,6 +43,7 @@ export const CopySourceTask = {
'typings/**',
'webpackShims/**',
'config/kibana.yml',
+ 'config/node.options',
'tsconfig*.json',
'.i18nrc.json',
'kibana.d.ts',
From 17dc0439e21737cfd9c4a598028251771cdfb5e0 Mon Sep 17 00:00:00 2001
From: Nicolas Chaulet
Date: Mon, 13 Jul 2020 11:33:51 -0400
Subject: [PATCH 10/40] [Ingest Manager] Add UI to enroll standalone agent
(#71288)
---
.../common/services/config_to_yaml.ts | 2 +-
.../hooks/use_request/agent_config.ts | 11 ++
.../hooks/use_request/enrollment_api_keys.ts | 12 ++
.../config_selection.tsx | 163 ++++++++++------
.../agent_enrollment_flyout/index.tsx | 119 +++---------
.../managed_instructions.tsx | 91 +++++++++
.../standalone_instructions.tsx | 181 ++++++++++++++++++
.../agent_enrollment_flyout/steps.tsx | 66 +++++++
.../server/routes/agent_config/handlers.ts | 21 +-
.../server/services/agent_config.ts | 10 +-
.../server/types/rest_spec/agent_config.ts | 1 +
.../translations/translations/ja-JP.json | 1 -
.../translations/translations/zh-CN.json | 1 -
13 files changed, 516 insertions(+), 163 deletions(-)
create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/managed_instructions.tsx
create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx
create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/steps.tsx
diff --git a/x-pack/plugins/ingest_manager/common/services/config_to_yaml.ts b/x-pack/plugins/ingest_manager/common/services/config_to_yaml.ts
index c2043a40369e2..7e03e4572f9ee 100644
--- a/x-pack/plugins/ingest_manager/common/services/config_to_yaml.ts
+++ b/x-pack/plugins/ingest_manager/common/services/config_to_yaml.ts
@@ -11,8 +11,8 @@ const CONFIG_KEYS_ORDER = [
'name',
'revision',
'type',
- 'settings',
'outputs',
+ 'settings',
'inputs',
'enabled',
'use_output',
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts
index 56b78c6faa93a..0bb09c2731032 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts
@@ -48,6 +48,17 @@ export const useGetOneAgentConfigFull = (agentConfigId: string) => {
});
};
+export const sendGetOneAgentConfigFull = (
+ agentConfigId: string,
+ query: { standalone?: boolean } = {}
+) => {
+ return sendRequest({
+ path: agentConfigRouteService.getInfoFullPath(agentConfigId),
+ method: 'get',
+ query,
+ });
+};
+
export const sendGetOneAgentConfig = (agentConfigId: string) => {
return sendRequest({
path: agentConfigRouteService.getInfoPath(agentConfigId),
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/enrollment_api_keys.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/enrollment_api_keys.ts
index 10d9e03e986e1..5a334e2739027 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/enrollment_api_keys.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/enrollment_api_keys.ts
@@ -44,6 +44,18 @@ export function sendDeleteOneEnrollmentAPIKey(keyId: string, options?: RequestOp
});
}
+export function sendGetEnrollmentAPIKeys(
+ query: GetEnrollmentAPIKeysRequest['query'],
+ options?: RequestOptions
+) {
+ return sendRequest({
+ method: 'get',
+ path: enrollmentAPIKeyRouteService.getListPath(),
+ query,
+ ...options,
+ });
+}
+
export function useGetEnrollmentAPIKeys(
query: GetEnrollmentAPIKeysRequest['query'],
options?: RequestOptions
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx
index 8cd337586d1bc..6f53a237187e5 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx
@@ -4,46 +4,91 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiSelect, EuiSpacer, EuiText, EuiButtonEmpty } from '@elastic/eui';
-import { AgentConfig } from '../../../../types';
-import { useGetEnrollmentAPIKeys } from '../../../../hooks';
+import { AgentConfig, GetEnrollmentAPIKeysResponse } from '../../../../types';
+import { sendGetEnrollmentAPIKeys, useCore } from '../../../../hooks';
import { AgentConfigPackageBadges } from '../agent_config_package_badges';
-interface Props {
+type Props = {
agentConfigs: AgentConfig[];
- onKeyChange: (key: string) => void;
-}
+ onConfigChange?: (key: string) => void;
+} & (
+ | {
+ withKeySelection: true;
+ onKeyChange?: (key: string) => void;
+ }
+ | {
+ withKeySelection: false;
+ }
+);
-export const EnrollmentStepAgentConfig: React.FC = ({ agentConfigs, onKeyChange }) => {
- const [isAuthenticationSettingsOpen, setIsAuthenticationSettingsOpen] = useState(false);
- const enrollmentAPIKeysRequest = useGetEnrollmentAPIKeys({
- page: 1,
- perPage: 1000,
- });
+export const EnrollmentStepAgentConfig: React.FC = (props) => {
+ const { notifications } = useCore();
+ const { withKeySelection, agentConfigs, onConfigChange } = props;
+ const onKeyChange = props.withKeySelection && props.onKeyChange;
+ const [isAuthenticationSettingsOpen, setIsAuthenticationSettingsOpen] = useState(false);
+ const [enrollmentAPIKeys, setEnrollmentAPIKeys] = useState(
+ []
+ );
const [selectedState, setSelectedState] = useState<{
agentConfigId?: string;
enrollmentAPIKeyId?: string;
}>({
agentConfigId: agentConfigs.length ? agentConfigs[0].id : undefined,
});
- const filteredEnrollmentAPIKeys = React.useMemo(() => {
- if (!selectedState.agentConfigId || !enrollmentAPIKeysRequest.data) {
- return [];
+
+ useEffect(() => {
+ if (onConfigChange && selectedState.agentConfigId) {
+ onConfigChange(selectedState.agentConfigId);
}
+ }, [selectedState.agentConfigId, onConfigChange]);
- return enrollmentAPIKeysRequest.data.list.filter(
- (key) => key.config_id === selectedState.agentConfigId
- );
- }, [enrollmentAPIKeysRequest.data, selectedState.agentConfigId]);
+ useEffect(() => {
+ if (!withKeySelection) {
+ return;
+ }
+ if (!selectedState.agentConfigId) {
+ setEnrollmentAPIKeys([]);
+ return;
+ }
+
+ async function fetchEnrollmentAPIKeys() {
+ try {
+ const res = await sendGetEnrollmentAPIKeys({
+ page: 1,
+ perPage: 10000,
+ });
+ if (res.error) {
+ throw res.error;
+ }
+
+ if (!res.data) {
+ throw new Error('No data while fetching enrollment API keys');
+ }
+
+ setEnrollmentAPIKeys(
+ res.data.list.filter((key) => key.config_id === selectedState.agentConfigId)
+ );
+ } catch (error) {
+ notifications.toasts.addError(error, {
+ title: 'Error',
+ });
+ }
+ }
+ fetchEnrollmentAPIKeys();
+ }, [withKeySelection, selectedState.agentConfigId, notifications.toasts]);
// Select first API key when config change
React.useEffect(() => {
- if (!selectedState.enrollmentAPIKeyId && filteredEnrollmentAPIKeys.length > 0) {
- const enrollmentAPIKeyId = filteredEnrollmentAPIKeys[0].id;
+ if (!withKeySelection || !onKeyChange) {
+ return;
+ }
+ if (!selectedState.enrollmentAPIKeyId && enrollmentAPIKeys.length > 0) {
+ const enrollmentAPIKeyId = enrollmentAPIKeys[0].id;
setSelectedState({
agentConfigId: selectedState.agentConfigId,
enrollmentAPIKeyId,
@@ -51,7 +96,7 @@ export const EnrollmentStepAgentConfig: React.FC = ({ agentConfigs, onKey
onKeyChange(enrollmentAPIKeyId);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [filteredEnrollmentAPIKeys, selectedState.enrollmentAPIKeyId, selectedState.agentConfigId]);
+ }, [enrollmentAPIKeys, selectedState.enrollmentAPIKeyId, selectedState.agentConfigId]);
return (
<>
@@ -85,43 +130,47 @@ export const EnrollmentStepAgentConfig: React.FC = ({ agentConfigs, onKey
{selectedState.agentConfigId && (
)}
-
- setIsAuthenticationSettingsOpen(!isAuthenticationSettingsOpen)}
- >
-
-
- {isAuthenticationSettingsOpen && (
+ {withKeySelection && onKeyChange && (
<>
- ({
- value: key.id,
- text: key.name,
- }))}
- value={selectedState.enrollmentAPIKeyId || undefined}
- prepend={
-
-
-
- }
- onChange={(e) => {
- setSelectedState({
- ...selectedState,
- enrollmentAPIKeyId: e.target.value,
- });
- onKeyChange(e.target.value);
- }}
- />
+ setIsAuthenticationSettingsOpen(!isAuthenticationSettingsOpen)}
+ >
+
+
+ {isAuthenticationSettingsOpen && (
+ <>
+
+ ({
+ value: key.id,
+ text: key.name,
+ }))}
+ value={selectedState.enrollmentAPIKeyId || undefined}
+ prepend={
+
+
+
+ }
+ onChange={(e) => {
+ setSelectedState({
+ ...selectedState,
+ enrollmentAPIKeyId: e.target.value,
+ });
+ onKeyChange(e.target.value);
+ }}
+ />
+ >
+ )}
>
)}
>
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx
index 43173124d6bae..5a9d3b7efe1bb 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx
@@ -14,23 +14,13 @@ import {
EuiButtonEmpty,
EuiButton,
EuiFlyoutFooter,
- EuiSteps,
- EuiText,
- EuiLink,
+ EuiTab,
+ EuiTabs,
} from '@elastic/eui';
-import { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps';
-import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { AgentConfig } from '../../../../types';
-import { EnrollmentStepAgentConfig } from './config_selection';
-import {
- useGetOneEnrollmentAPIKey,
- useCore,
- useGetSettings,
- useLink,
- useFleetStatus,
-} from '../../../../hooks';
-import { ManualInstructions } from '../../../../components/enrollment_instructions';
+import { ManagedInstructions } from './managed_instructions';
+import { StandaloneInstructions } from './standalone_instructions';
interface Props {
onClose: () => void;
@@ -41,99 +31,40 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({
onClose,
agentConfigs = [],
}) => {
- const { getHref } = useLink();
- const core = useCore();
- const fleetStatus = useFleetStatus();
-
- const [selectedAPIKeyId, setSelectedAPIKeyId] = useState();
-
- const settings = useGetSettings();
- const apiKey = useGetOneEnrollmentAPIKey(selectedAPIKeyId);
-
- const kibanaUrl =
- settings.data?.item?.kibana_url ?? `${window.location.origin}${core.http.basePath.get()}`;
- const kibanaCASha256 = settings.data?.item?.kibana_ca_sha256;
-
- const steps: EuiContainedStepProps[] = [
- {
- title: i18n.translate('xpack.ingestManager.agentEnrollment.stepDownloadAgentTitle', {
- defaultMessage: 'Download the Elastic Agent',
- }),
- children: (
-
-
-
-
- ),
- }}
- />
-
- ),
- },
- {
- title: i18n.translate('xpack.ingestManager.agentEnrollment.stepChooseAgentConfigTitle', {
- defaultMessage: 'Choose an agent configuration',
- }),
- children: (
-
- ),
- },
- {
- title: i18n.translate('xpack.ingestManager.agentEnrollment.stepRunAgentTitle', {
- defaultMessage: 'Enroll and run the Elastic Agent',
- }),
- children: apiKey.data && (
-
- ),
- },
- ];
+ const [mode, setMode] = useState<'managed' | 'standalone'>('managed');
return (
-
+
+
+ setMode('managed')}>
+
+
+ setMode('standalone')}>
+
+
+
+
- {fleetStatus.isReady ? (
- <>
-
- >
+ {mode === 'managed' ? (
+
) : (
- <>
-
-
-
- ),
- }}
- />
- >
+
)}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/managed_instructions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/managed_instructions.tsx
new file mode 100644
index 0000000000000..aabbd37e809a8
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/managed_instructions.tsx
@@ -0,0 +1,91 @@
+/*
+ * 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 } from 'react';
+import { EuiSteps, EuiLink, EuiText, EuiSpacer } from '@elastic/eui';
+import { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { AgentConfig } from '../../../../types';
+import {
+ useGetOneEnrollmentAPIKey,
+ useCore,
+ useGetSettings,
+ useLink,
+ useFleetStatus,
+} from '../../../../hooks';
+import { ManualInstructions } from '../../../../components/enrollment_instructions';
+import { DownloadStep, AgentConfigSelectionStep } from './steps';
+
+interface Props {
+ agentConfigs: AgentConfig[];
+}
+
+export const ManagedInstructions: React.FunctionComponent = ({ agentConfigs = [] }) => {
+ const { getHref } = useLink();
+ const core = useCore();
+ const fleetStatus = useFleetStatus();
+
+ const [selectedAPIKeyId, setSelectedAPIKeyId] = useState();
+
+ const settings = useGetSettings();
+ const apiKey = useGetOneEnrollmentAPIKey(selectedAPIKeyId);
+
+ const kibanaUrl =
+ settings.data?.item?.kibana_url ?? `${window.location.origin}${core.http.basePath.get()}`;
+ const kibanaCASha256 = settings.data?.item?.kibana_ca_sha256;
+
+ const steps: EuiContainedStepProps[] = [
+ DownloadStep(),
+ AgentConfigSelectionStep({ agentConfigs, setSelectedAPIKeyId }),
+ {
+ title: i18n.translate('xpack.ingestManager.agentEnrollment.stepEnrollAndRunAgentTitle', {
+ defaultMessage: 'Enroll and start the Elastic Agent',
+ }),
+ children: apiKey.data && (
+
+ ),
+ },
+ ];
+
+ return (
+ <>
+
+
+
+
+ {fleetStatus.isReady ? (
+ <>
+
+ >
+ ) : (
+ <>
+
+
+
+ ),
+ }}
+ />
+ >
+ )}{' '}
+ >
+ );
+};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx
new file mode 100644
index 0000000000000..27f64059deb84
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx
@@ -0,0 +1,181 @@
+/*
+ * 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, useMemo } from 'react';
+import {
+ EuiSteps,
+ EuiText,
+ EuiSpacer,
+ EuiButton,
+ EuiCode,
+ EuiFlexItem,
+ EuiFlexGroup,
+ EuiCodeBlock,
+ EuiCopy,
+} from '@elastic/eui';
+import { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { AgentConfig } from '../../../../types';
+import { useCore, sendGetOneAgentConfigFull } from '../../../../hooks';
+import { DownloadStep, AgentConfigSelectionStep } from './steps';
+import { configToYaml, agentConfigRouteService } from '../../../../services';
+
+interface Props {
+ agentConfigs: AgentConfig[];
+}
+
+const RUN_INSTRUCTIONS = './elastic-agent run';
+
+export const StandaloneInstructions: React.FunctionComponent = ({ agentConfigs = [] }) => {
+ const core = useCore();
+ const { notifications } = core;
+
+ const [selectedConfigId, setSelectedConfigId] = useState();
+ const [fullAgentConfig, setFullAgentConfig] = useState();
+
+ const downloadLink = selectedConfigId
+ ? core.http.basePath.prepend(
+ `${agentConfigRouteService.getInfoFullDownloadPath(selectedConfigId)}?standalone=true`
+ )
+ : undefined;
+
+ useEffect(() => {
+ async function fetchFullConfig() {
+ try {
+ if (!selectedConfigId) {
+ return;
+ }
+ const res = await sendGetOneAgentConfigFull(selectedConfigId, { standalone: true });
+ if (res.error) {
+ throw res.error;
+ }
+
+ if (!res.data) {
+ throw new Error('No data while fetching full agent config');
+ }
+
+ setFullAgentConfig(res.data.item);
+ } catch (error) {
+ notifications.toasts.addError(error, {
+ title: 'Error',
+ });
+ }
+ }
+ fetchFullConfig();
+ }, [selectedConfigId, notifications.toasts]);
+
+ const yaml = useMemo(() => configToYaml(fullAgentConfig), [fullAgentConfig]);
+ const steps: EuiContainedStepProps[] = [
+ DownloadStep(),
+ AgentConfigSelectionStep({ agentConfigs, setSelectedConfigId }),
+ {
+ title: i18n.translate('xpack.ingestManager.agentEnrollment.stepConfigureAgentTitle', {
+ defaultMessage: 'Configure the agent',
+ }),
+ children: (
+ <>
+
+ elastic-agent.yml,
+ ESUsernameVariable: ES_USERNAME,
+ ESPasswordVariable: ES_PASSWORD,
+ outputSection: outputs,
+ }}
+ />
+
+
+
+
+ {(copy) => (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ {yaml}
+
+
+ >
+ ),
+ },
+ {
+ title: i18n.translate('xpack.ingestManager.agentEnrollment.stepRunAgentTitle', {
+ defaultMessage: 'Start the agent',
+ }),
+ children: (
+ <>
+
+
+
+ {RUN_INSTRUCTIONS}
+
+
+ {(copy) => (
+
+
+
+ )}
+
+
+ >
+ ),
+ },
+ {
+ title: i18n.translate('xpack.ingestManager.agentEnrollment.stepCheckForDataTitle', {
+ defaultMessage: 'Check for data',
+ }),
+ status: 'incomplete',
+ children: (
+ <>
+
+
+
+ >
+ ),
+ },
+ ];
+
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/steps.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/steps.tsx
new file mode 100644
index 0000000000000..267f9027a094a
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/steps.tsx
@@ -0,0 +1,66 @@
+/*
+ * 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 { EuiText, EuiButton, EuiSpacer } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+import { EnrollmentStepAgentConfig } from './config_selection';
+import { AgentConfig } from '../../../../types';
+
+export const DownloadStep = () => {
+ return {
+ title: i18n.translate('xpack.ingestManager.agentEnrollment.stepDownloadAgentTitle', {
+ defaultMessage: 'Download the Elastic Agent',
+ }),
+ children: (
+ <>
+
+
+
+
+
+
+
+ >
+ ),
+ };
+};
+
+export const AgentConfigSelectionStep = ({
+ agentConfigs,
+ setSelectedAPIKeyId,
+ setSelectedConfigId,
+}: {
+ agentConfigs: AgentConfig[];
+ setSelectedAPIKeyId?: (key: string) => void;
+ setSelectedConfigId?: (configId: string) => void;
+}) => {
+ return {
+ title: i18n.translate('xpack.ingestManager.agentEnrollment.stepChooseAgentConfigTitle', {
+ defaultMessage: 'Choose an agent configuration',
+ }),
+ children: (
+
+ ),
+ };
+};
diff --git a/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts
index 110f6b9950829..2aaf889296bd6 100644
--- a/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts
@@ -232,15 +232,17 @@ export const deleteAgentConfigsHandler: RequestHandler<
}
};
-export const getFullAgentConfig: RequestHandler> = async (context, request, response) => {
+export const getFullAgentConfig: RequestHandler<
+ TypeOf,
+ TypeOf
+> = async (context, request, response) => {
const soClient = context.core.savedObjects.client;
try {
const fullAgentConfig = await agentConfigService.getFullConfig(
soClient,
- request.params.agentConfigId
+ request.params.agentConfigId,
+ { standalone: request.query.standalone === true }
);
if (fullAgentConfig) {
const body: GetFullAgentConfigResponse = {
@@ -264,16 +266,19 @@ export const getFullAgentConfig: RequestHandler> = async (context, request, response) => {
+export const downloadFullAgentConfig: RequestHandler<
+ TypeOf,
+ TypeOf
+> = async (context, request, response) => {
const soClient = context.core.savedObjects.client;
const {
params: { agentConfigId },
} = request;
try {
- const fullAgentConfig = await agentConfigService.getFullConfig(soClient, agentConfigId);
+ const fullAgentConfig = await agentConfigService.getFullConfig(soClient, agentConfigId, {
+ standalone: request.query.standalone === true,
+ });
if (fullAgentConfig) {
const body = configToYaml(fullAgentConfig);
const headers: ResponseHeaders = {
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 fe247d5b91db0..5f98c8881388d 100644
--- a/x-pack/plugins/ingest_manager/server/services/agent_config.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts
@@ -365,7 +365,8 @@ class AgentConfigService {
public async getFullConfig(
soClient: SavedObjectsClientContract,
- id: string
+ id: string,
+ options?: { standalone: boolean }
): Promise {
let config;
@@ -400,6 +401,13 @@ class AgentConfigService {
api_key,
...outputConfig,
};
+
+ if (options?.standalone) {
+ delete outputs[name].api_key;
+ outputs[name].username = 'ES_USERNAME';
+ outputs[name].password = 'ES_PASSWORD';
+ }
+
return outputs;
},
{} as FullAgentConfig['outputs']
diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts
index d076a803f4b53..594bd141459c1 100644
--- a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts
+++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts
@@ -51,5 +51,6 @@ export const GetFullAgentConfigRequestSchema = {
}),
query: schema.object({
download: schema.maybe(schema.boolean()),
+ standalone: schema.maybe(schema.boolean()),
}),
};
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 97afa9e058b98..e28ef8ff07bdd 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -8042,7 +8042,6 @@
"xpack.ingestManager.agentDetails.viewAgentListTitle": "すべてのエージェント構成を表示",
"xpack.ingestManager.agentEnrollment.cancelButtonLabel": "キャンセル",
"xpack.ingestManager.agentEnrollment.continueButtonLabel": "続行",
- "xpack.ingestManager.agentEnrollment.downloadDescription": "ホストのコンピューターでElasticエージェントをダウンロードします。エージェントバイナリをダウンロードできます。検証署名はElasticの{downloadLink}にあります。",
"xpack.ingestManager.agentEnrollment.downloadLink": "ダウンロードページ",
"xpack.ingestManager.agentEnrollment.fleetNotInitializedText": "エージェントを登録する前に、フリートを設定する必要があります。{link}",
"xpack.ingestManager.agentEnrollment.flyoutTitle": "新しいエージェントを登録",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 31a33bfa09b64..1df676ba7cffd 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -8047,7 +8047,6 @@
"xpack.ingestManager.agentDetails.viewAgentListTitle": "查看所有代理配置",
"xpack.ingestManager.agentEnrollment.cancelButtonLabel": "取消",
"xpack.ingestManager.agentEnrollment.continueButtonLabel": "继续",
- "xpack.ingestManager.agentEnrollment.downloadDescription": "在主机计算机上下载 Elastic 代理。可以从 Elastic 的{downloadLink}下载代理二进制文件及其验证签名。",
"xpack.ingestManager.agentEnrollment.downloadLink": "下载页面",
"xpack.ingestManager.agentEnrollment.fleetNotInitializedText": "注册代理前需要设置 Fleet。{link}",
"xpack.ingestManager.agentEnrollment.flyoutTitle": "注册新代理",
From b435d4608036c89439d1cfec6349ac7bc22adbca Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Mon, 13 Jul 2020 16:43:40 +0100
Subject: [PATCH 11/40] skip flaky suite (#71361)
---
.../cypress/integration/timeline_toggle_column.spec.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts
index 12e6f3db9b61e..759eec69bc022 100644
--- a/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts
@@ -24,7 +24,8 @@ import {
import { HOSTS_URL } from '../urls/navigation';
-describe('toggle column in timeline', () => {
+// Flaky: https://github.com/elastic/kibana/issues/71361
+describe.skip('toggle column in timeline', () => {
before(() => {
loginAndWaitForPage(HOSTS_URL);
});
From 2c19feb55fb45bd68bfa34eeb0c4ca5fda0726d9 Mon Sep 17 00:00:00 2001
From: Jonathan Budzenski
Date: Mon, 13 Jul 2020 10:51:30 -0500
Subject: [PATCH 12/40] [os packages] local permission adjustments (#66614)
* outline permissions
* rm keystore setup
Co-authored-by: Elastic Machine
---
src/dev/build/tasks/bin/scripts/kibana-keystore.bat | 0
.../tasks/os_packages/package_scripts/post_install.sh | 10 +++++++++-
.../service_templates/sysv/etc/init.d/kibana | 2 +-
3 files changed, 10 insertions(+), 2 deletions(-)
mode change 100644 => 100755 src/dev/build/tasks/bin/scripts/kibana-keystore.bat
diff --git a/src/dev/build/tasks/bin/scripts/kibana-keystore.bat b/src/dev/build/tasks/bin/scripts/kibana-keystore.bat
old mode 100644
new mode 100755
diff --git a/src/dev/build/tasks/os_packages/package_scripts/post_install.sh b/src/dev/build/tasks/os_packages/package_scripts/post_install.sh
index 9cf08ea38254d..10f11ff51874e 100644
--- a/src/dev/build/tasks/os_packages/package_scripts/post_install.sh
+++ b/src/dev/build/tasks/os_packages/package_scripts/post_install.sh
@@ -1,6 +1,8 @@
#!/bin/sh
set -e
+export KBN_PATH_CONF=${KBN_PATH_CONF:-<%= configDir %>}
+
case $1 in
# Debian
configure)
@@ -35,4 +37,10 @@ case $1 in
esac
chown -R <%= user %>:<%= group %> <%= dataDir %>
-chown <%= user %>:<%= group %> <%= pluginsDir %>
+chmod 2750 <%= dataDir %>
+chmod -R 2755 <%= dataDir %>/*
+
+chown :<%= group %> ${KBN_PATH_CONF}
+chown :<%= group %> ${KBN_PATH_CONF}/kibana.yml
+chmod 2750 ${KBN_PATH_CONF}
+chmod 660 ${KBN_PATH_CONF}/kibana.yml
diff --git a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana
index d935dc6e31f80..8facbb709cc5c 100755
--- a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana
+++ b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana
@@ -39,7 +39,7 @@ emit() {
start() {
[ ! -d "/var/log/kibana/" ] && mkdir "/var/log/kibana/"
chown "$user":"$group" "/var/log/kibana/"
- chmod 755 "/var/log/kibana/"
+ chmod 2750 "/var/log/kibana/"
[ ! -d "/var/run/kibana/" ] && mkdir "/var/run/kibana/"
chown "$user":"$group" "/var/run/kibana/"
From c44f01979019d32500856753ca35d67e542030c5 Mon Sep 17 00:00:00 2001
From: Thomas Neirynck
Date: Mon, 13 Jul 2020 11:55:36 -0400
Subject: [PATCH 13/40] [Maps] Show joins disabled message (#70826)
Show feedback in the layer-settings when the scaling-method does not support Term-joins.
---
.../data_request_descriptor_types.ts | 5 +-
.../maps/common/descriptor_types/sources.ts | 6 +-
.../blended_vector_layer.ts | 16 ++-
.../maps/public/classes/layers/layer.tsx | 22 ++--
.../es_geo_grid_source/es_geo_grid_source.js | 4 +-
.../es_pew_pew_source/es_pew_pew_source.js | 2 +-
.../es_search_source/es_search_source.js | 11 +-
.../maps/public/classes/sources/source.ts | 10 +-
.../sources/vector_source/vector_source.js | 2 +-
.../__snapshots__/view.test.js.snap | 13 +-
.../connected_components/layer_panel/index.js | 2 +-
.../__snapshots__/join_editor.test.tsx.snap | 100 ++++++++++++++
.../layer_panel/join_editor/index.js | 31 -----
.../layer_panel/join_editor/index.tsx | 31 +++++
.../join_editor/join_editor.test.tsx | 63 +++++++++
.../layer_panel/join_editor/join_editor.tsx | 124 ++++++++++++++++++
.../layer_panel/join_editor/view.js | 103 ---------------
.../connected_components/layer_panel/view.js | 5 +-
.../layer_panel/view.test.js | 2 +-
19 files changed, 383 insertions(+), 169 deletions(-)
create mode 100644 x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/__snapshots__/join_editor.test.tsx.snap
delete mode 100644 x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/index.js
create mode 100644 x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/index.tsx
create mode 100644 x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/join_editor.test.tsx
create mode 100644 x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/join_editor.tsx
delete mode 100644 x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/view.js
diff --git a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts
index c7bfe94742bd6..1bd8c5401eb1d 100644
--- a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts
+++ b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts
@@ -5,7 +5,7 @@
*/
/* eslint-disable @typescript-eslint/consistent-type-definitions */
-import { RENDER_AS, SORT_ORDER, SCALING_TYPES } from '../constants';
+import { RENDER_AS, SORT_ORDER, SCALING_TYPES, SOURCE_TYPES } from '../constants';
import { MapExtent, MapQuery } from './map_descriptor';
import { Filter, TimeRange } from '../../../../../src/plugins/data/common';
@@ -26,10 +26,12 @@ type ESSearchSourceSyncMeta = {
scalingType: SCALING_TYPES;
topHitsSplitField: string;
topHitsSize: number;
+ sourceType: SOURCE_TYPES.ES_SEARCH;
};
type ESGeoGridSourceSyncMeta = {
requestType: RENDER_AS;
+ sourceType: SOURCE_TYPES.ES_GEO_GRID;
};
export type VectorSourceSyncMeta = ESSearchSourceSyncMeta | ESGeoGridSourceSyncMeta | null;
@@ -51,7 +53,6 @@ export type VectorStyleRequestMeta = MapFilters & {
export type ESSearchSourceResponseMeta = {
areResultsTrimmed?: boolean;
- sourceType?: string;
// top hits meta
areEntitiesTrimmed?: boolean;
diff --git a/x-pack/plugins/maps/common/descriptor_types/sources.ts b/x-pack/plugins/maps/common/descriptor_types/sources.ts
index e32b5f44c8272..7eda37bf53351 100644
--- a/x-pack/plugins/maps/common/descriptor_types/sources.ts
+++ b/x-pack/plugins/maps/common/descriptor_types/sources.ts
@@ -77,8 +77,8 @@ export type ESPewPewSourceDescriptor = AbstractESAggSourceDescriptor & {
};
export type ESTermSourceDescriptor = AbstractESAggSourceDescriptor & {
- indexPatternTitle: string;
- term: string; // term field name
+ indexPatternTitle?: string;
+ term?: string; // term field name
whereQuery?: Query;
};
@@ -138,7 +138,7 @@ export type GeojsonFileSourceDescriptor = {
};
export type JoinDescriptor = {
- leftField: string;
+ leftField?: string;
right: ESTermSourceDescriptor;
};
diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts
index 551e20fc5ceb5..26a0ffc1b1a37 100644
--- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts
+++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts
@@ -126,7 +126,7 @@ function getClusterStyleDescriptor(
),
}
: undefined;
- // @ts-ignore
+ // @ts-expect-error
clusterStyleDescriptor.properties[styleName] = {
type: STYLE_TYPE.DYNAMIC,
options: {
@@ -136,7 +136,7 @@ function getClusterStyleDescriptor(
};
} else {
// copy static styles to cluster style
- // @ts-ignore
+ // @ts-expect-error
clusterStyleDescriptor.properties[styleName] = {
type: STYLE_TYPE.STATIC,
options: { ...styleProperty.getOptions() },
@@ -192,8 +192,8 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer {
const requestMeta = sourceDataRequest.getMeta();
if (
requestMeta &&
- requestMeta.sourceType &&
- requestMeta.sourceType === SOURCE_TYPES.ES_GEO_GRID
+ requestMeta.sourceMeta &&
+ requestMeta.sourceMeta.sourceType === SOURCE_TYPES.ES_GEO_GRID
) {
isClustered = true;
}
@@ -220,8 +220,12 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer {
: displayName;
}
- isJoinable() {
- return false;
+ showJoinEditor() {
+ return true;
+ }
+
+ getJoinsDisabledReason() {
+ return this._documentSource.getJoinsDisabledReason();
}
getJoins() {
diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx
index d6f6ee8fa609b..d8def155a9185 100644
--- a/x-pack/plugins/maps/public/classes/layers/layer.tsx
+++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx
@@ -78,6 +78,8 @@ export interface ILayer {
isPreviewLayer: () => boolean;
areLabelsOnTop: () => boolean;
supportsLabelsOnTop: () => boolean;
+ showJoinEditor(): boolean;
+ getJoinsDisabledReason(): string | null;
}
export type Footnote = {
icon: ReactElement;
@@ -141,13 +143,12 @@ export class AbstractLayer implements ILayer {
}
static getBoundDataForSource(mbMap: unknown, sourceId: string): FeatureCollection {
- // @ts-ignore
+ // @ts-expect-error
const mbStyle = mbMap.getStyle();
return mbStyle.sources[sourceId].data;
}
async cloneDescriptor(): Promise {
- // @ts-ignore
const clonedDescriptor = copyPersistentState(this._descriptor);
// layer id is uuid used to track styles/layers in mapbox
clonedDescriptor.id = uuid();
@@ -155,14 +156,10 @@ export class AbstractLayer implements ILayer {
clonedDescriptor.label = `Clone of ${displayName}`;
clonedDescriptor.sourceDescriptor = this.getSource().cloneDescriptor();
- // todo: remove this
- // This should not be in AbstractLayer. It relies on knowledge of VectorLayerDescriptor
- // @ts-ignore
if (clonedDescriptor.joins) {
- // @ts-ignore
+ // @ts-expect-error
clonedDescriptor.joins.forEach((joinDescriptor) => {
// right.id is uuid used to track requests in inspector
- // @ts-ignore
joinDescriptor.right.id = uuid();
});
}
@@ -173,8 +170,12 @@ export class AbstractLayer implements ILayer {
return `${this.getId()}${MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER}${layerNameSuffix}`;
}
- isJoinable(): boolean {
- return this.getSource().isJoinable();
+ showJoinEditor(): boolean {
+ return this.getSource().showJoinEditor();
+ }
+
+ getJoinsDisabledReason() {
+ return this.getSource().getJoinsDisabledReason();
}
isPreviewLayer(): boolean {
@@ -394,7 +395,6 @@ export class AbstractLayer implements ILayer {
const requestTokens = this._dataRequests.map((dataRequest) => dataRequest.getRequestToken());
// Compact removes all the undefineds
- // @ts-ignore
return _.compact(requestTokens);
}
@@ -478,7 +478,7 @@ export class AbstractLayer implements ILayer {
}
syncVisibilityWithMb(mbMap: unknown, mbLayerId: string) {
- // @ts-ignore
+ // @ts-expect-error
mbMap.setLayoutProperty(mbLayerId, 'visibility', this.isVisible() ? 'visible' : 'none');
}
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js
index 9431fb55dc88b..1be74140fe1bf 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js
+++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js
@@ -63,6 +63,7 @@ export class ESGeoGridSource extends AbstractESAggSource {
getSyncMeta() {
return {
requestType: this._descriptor.requestType,
+ sourceType: SOURCE_TYPES.ES_GEO_GRID,
};
}
@@ -103,7 +104,7 @@ export class ESGeoGridSource extends AbstractESAggSource {
return true;
}
- isJoinable() {
+ showJoinEditor() {
return false;
}
@@ -307,7 +308,6 @@ export class ESGeoGridSource extends AbstractESAggSource {
},
meta: {
areResultsTrimmed: false,
- sourceType: SOURCE_TYPES.ES_GEO_GRID,
},
};
}
diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js
index a4cff7c89a011..98db7bcdcc8a3 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js
+++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js
@@ -51,7 +51,7 @@ export class ESPewPewSource extends AbstractESAggSource {
return true;
}
- isJoinable() {
+ showJoinEditor() {
return false;
}
diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js
index c8f14f1dc6a4b..330fa6e8318ed 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js
+++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js
@@ -385,7 +385,7 @@ export class ESSearchSource extends AbstractESSource {
return {
data: featureCollection,
- meta: { ...meta, sourceType: SOURCE_TYPES.ES_SEARCH },
+ meta,
};
}
@@ -540,6 +540,7 @@ export class ESSearchSource extends AbstractESSource {
scalingType: this._descriptor.scalingType,
topHitsSplitField: this._descriptor.topHitsSplitField,
topHitsSize: this._descriptor.topHitsSize,
+ sourceType: SOURCE_TYPES.ES_SEARCH,
};
}
@@ -551,6 +552,14 @@ export class ESSearchSource extends AbstractESSource {
path: geoField.name,
};
}
+
+ getJoinsDisabledReason() {
+ return this._descriptor.scalingType === SCALING_TYPES.CLUSTERS
+ ? i18n.translate('xpack.maps.source.esSearch.joinsDisabledReason', {
+ defaultMessage: 'Joins are not supported when scaling by clusters',
+ })
+ : null;
+ }
}
registerSource({
diff --git a/x-pack/plugins/maps/public/classes/sources/source.ts b/x-pack/plugins/maps/public/classes/sources/source.ts
index c68e22ada8b0c..696c07376575b 100644
--- a/x-pack/plugins/maps/public/classes/sources/source.ts
+++ b/x-pack/plugins/maps/public/classes/sources/source.ts
@@ -54,7 +54,8 @@ export interface ISource {
isESSource(): boolean;
renderSourceSettingsEditor({ onChange }: SourceEditorArgs): ReactElement | null;
supportsFitToBounds(): Promise;
- isJoinable(): boolean;
+ showJoinEditor(): boolean;
+ getJoinsDisabledReason(): string | null;
cloneDescriptor(): SourceDescriptor;
getFieldNames(): string[];
getApplyGlobalQuery(): boolean;
@@ -80,7 +81,6 @@ export class AbstractSource implements ISource {
destroy(): void {}
cloneDescriptor(): SourceDescriptor {
- // @ts-ignore
return copyPersistentState(this._descriptor);
}
@@ -148,10 +148,14 @@ export class AbstractSource implements ISource {
return 0;
}
- isJoinable(): boolean {
+ showJoinEditor(): boolean {
return false;
}
+ getJoinsDisabledReason() {
+ return null;
+ }
+
isESSource(): boolean {
return false;
}
diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.js b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.js
index ecb13bb875721..98ed89a6ff0ad 100644
--- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.js
+++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.js
@@ -122,7 +122,7 @@ export class AbstractVectorSource extends AbstractSource {
return false;
}
- isJoinable() {
+ showJoinEditor() {
return true;
}
diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap b/x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap
index 1c48ed2290dce..2cf5287ae6594 100644
--- a/x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap
+++ b/x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap
@@ -96,8 +96,8 @@ exports[`LayerPanel is rendered 1`] = `
"getId": [Function],
"getImmutableSourceProperties": [Function],
"getLayerTypeIconName": [Function],
- "isJoinable": [Function],
"renderSourceSettingsEditor": [Function],
+ "showJoinEditor": [Function],
"supportsElasticsearchFilters": [Function],
}
}
@@ -107,6 +107,17 @@ exports[`LayerPanel is rendered 1`] = `
diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/index.js b/x-pack/plugins/maps/public/connected_components/layer_panel/index.js
index 1c8dcdb43d434..17fd41d120194 100644
--- a/x-pack/plugins/maps/public/connected_components/layer_panel/index.js
+++ b/x-pack/plugins/maps/public/connected_components/layer_panel/index.js
@@ -12,7 +12,7 @@ import { updateSourceProp } from '../../actions';
function mapStateToProps(state = {}) {
const selectedLayer = getSelectedLayer(state);
return {
- key: selectedLayer ? `${selectedLayer.getId()}${selectedLayer.isJoinable()}` : '',
+ key: selectedLayer ? `${selectedLayer.getId()}${selectedLayer.showJoinEditor()}` : '',
selectedLayer,
};
}
diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/__snapshots__/join_editor.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/__snapshots__/join_editor.test.tsx.snap
new file mode 100644
index 0000000000000..00d7f44d6273f
--- /dev/null
+++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/__snapshots__/join_editor.test.tsx.snap
@@ -0,0 +1,100 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Should render callout when joins are disabled 1`] = `
+
+
+
+
+
+
+
+
+
+ Simulated disabled reason
+
+
+`;
+
+exports[`Should render join editor 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/index.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/index.js
deleted file mode 100644
index cf55c16bbe0be..0000000000000
--- a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/index.js
+++ /dev/null
@@ -1,31 +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 { connect } from 'react-redux';
-import { JoinEditor } from './view';
-import {
- getSelectedLayer,
- getSelectedLayerJoinDescriptors,
-} from '../../../selectors/map_selectors';
-import { setJoinsForLayer } from '../../../actions';
-
-function mapDispatchToProps(dispatch) {
- return {
- onChange: (layer, joins) => {
- dispatch(setJoinsForLayer(layer, joins));
- },
- };
-}
-
-function mapStateToProps(state = {}) {
- return {
- joins: getSelectedLayerJoinDescriptors(state),
- layer: getSelectedLayer(state),
- };
-}
-
-const connectedJoinEditor = connect(mapStateToProps, mapDispatchToProps)(JoinEditor);
-export { connectedJoinEditor as JoinEditor };
diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/index.tsx b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/index.tsx
new file mode 100644
index 0000000000000..0348b38351971
--- /dev/null
+++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/index.tsx
@@ -0,0 +1,31 @@
+/*
+ * 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 { AnyAction, Dispatch } from 'redux';
+import { connect } from 'react-redux';
+import { JoinEditor } from './join_editor';
+import { getSelectedLayerJoinDescriptors } from '../../../selectors/map_selectors';
+import { setJoinsForLayer } from '../../../actions';
+import { MapStoreState } from '../../../reducers/store';
+import { ILayer } from '../../../classes/layers/layer';
+import { JoinDescriptor } from '../../../../common/descriptor_types';
+
+function mapStateToProps(state: MapStoreState) {
+ return {
+ joins: getSelectedLayerJoinDescriptors(state),
+ };
+}
+
+function mapDispatchToProps(dispatch: Dispatch) {
+ return {
+ onChange: (layer: ILayer, joins: JoinDescriptor[]) => {
+ dispatch(setJoinsForLayer(layer, joins));
+ },
+ };
+}
+
+const connectedJoinEditor = connect(mapStateToProps, mapDispatchToProps)(JoinEditor);
+export { connectedJoinEditor as JoinEditor };
diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/join_editor.test.tsx b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/join_editor.test.tsx
new file mode 100644
index 0000000000000..12da1c4bb9388
--- /dev/null
+++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/join_editor.test.tsx
@@ -0,0 +1,63 @@
+/*
+ * 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 { ILayer } from '../../../classes/layers/layer';
+import { JoinEditor } from './join_editor';
+import { shallow } from 'enzyme';
+import { JoinDescriptor } from '../../../../common/descriptor_types';
+
+class MockLayer {
+ private readonly _disableReason: string | null;
+
+ constructor(disableReason: string | null) {
+ this._disableReason = disableReason;
+ }
+
+ getJoinsDisabledReason() {
+ return this._disableReason;
+ }
+}
+
+const defaultProps = {
+ joins: [
+ {
+ leftField: 'iso2',
+ right: {
+ id: '673ff994-fc75-4c67-909b-69fcb0e1060e',
+ indexPatternTitle: 'kibana_sample_data_logs',
+ term: 'geo.src',
+ indexPatternId: 'abcde',
+ metrics: [
+ {
+ type: 'count',
+ label: 'web logs count',
+ },
+ ],
+ },
+ } as JoinDescriptor,
+ ],
+ layerDisplayName: 'myLeftJoinField',
+ leftJoinFields: [],
+ onChange: () => {},
+};
+
+test('Should render join editor', () => {
+ const component = shallow(
+
+ );
+ expect(component).toMatchSnapshot();
+});
+
+test('Should render callout when joins are disabled', () => {
+ const component = shallow(
+
+ );
+ expect(component).toMatchSnapshot();
+});
diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/join_editor.tsx b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/join_editor.tsx
new file mode 100644
index 0000000000000..c589604e85112
--- /dev/null
+++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/join_editor.tsx
@@ -0,0 +1,124 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { Fragment } from 'react';
+import uuid from 'uuid/v4';
+
+import {
+ EuiButtonEmpty,
+ EuiTitle,
+ EuiSpacer,
+ EuiToolTip,
+ EuiTextAlign,
+ EuiCallOut,
+} from '@elastic/eui';
+
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+// @ts-expect-error
+import { Join } from './resources/join';
+import { ILayer } from '../../../classes/layers/layer';
+import { JoinDescriptor } from '../../../../common/descriptor_types';
+import { IField } from '../../../classes/fields/field';
+
+interface Props {
+ joins: JoinDescriptor[];
+ layer: ILayer;
+ layerDisplayName: string;
+ leftJoinFields: IField[];
+ onChange: (layer: ILayer, joins: JoinDescriptor[]) => void;
+}
+
+export function JoinEditor({ joins, layer, onChange, leftJoinFields, layerDisplayName }: Props) {
+ const renderJoins = () => {
+ return joins.map((joinDescriptor: JoinDescriptor, index: number) => {
+ const handleOnChange = (updatedDescriptor: JoinDescriptor) => {
+ onChange(layer, [...joins.slice(0, index), updatedDescriptor, ...joins.slice(index + 1)]);
+ };
+
+ const handleOnRemove = () => {
+ onChange(layer, [...joins.slice(0, index), ...joins.slice(index + 1)]);
+ };
+
+ return (
+
+
+
+
+ );
+ });
+ };
+
+ const addJoin = () => {
+ onChange(layer, [
+ ...joins,
+ {
+ right: {
+ id: uuid(),
+ applyGlobalQuery: true,
+ },
+ } as JoinDescriptor,
+ ]);
+ };
+
+ const renderContent = () => {
+ const disabledReason = layer.getJoinsDisabledReason();
+ return disabledReason ? (
+ {disabledReason}
+ ) : (
+
+ {renderJoins()}
+
+
+
+
+
+
+
+
+
+ );
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ {renderContent()}
+
+ );
+}
diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/view.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/view.js
deleted file mode 100644
index 900f5c9ff53ea..0000000000000
--- a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/view.js
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React, { Fragment } from 'react';
-import uuid from 'uuid/v4';
-
-import {
- EuiFlexGroup,
- EuiFlexItem,
- EuiButtonIcon,
- EuiTitle,
- EuiSpacer,
- EuiToolTip,
-} from '@elastic/eui';
-
-import { Join } from './resources/join';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { i18n } from '@kbn/i18n';
-
-export function JoinEditor({ joins, layer, onChange, leftJoinFields, layerDisplayName }) {
- const renderJoins = () => {
- return joins.map((joinDescriptor, index) => {
- const handleOnChange = (updatedDescriptor) => {
- onChange(layer, [...joins.slice(0, index), updatedDescriptor, ...joins.slice(index + 1)]);
- };
-
- const handleOnRemove = () => {
- onChange(layer, [...joins.slice(0, index), ...joins.slice(index + 1)]);
- };
-
- return (
-
-
-
-
- );
- });
- };
-
- const addJoin = () => {
- onChange(layer, [
- ...joins,
- {
- right: {
- id: uuid(),
- applyGlobalQuery: true,
- },
- },
- ]);
- };
-
- if (!layer.isJoinable()) {
- return null;
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {renderJoins()}
-
- );
-}
diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/view.js b/x-pack/plugins/maps/public/connected_components/layer_panel/view.js
index 71d76ff53d8a9..2e20a4492f08b 100644
--- a/x-pack/plugins/maps/public/connected_components/layer_panel/view.js
+++ b/x-pack/plugins/maps/public/connected_components/layer_panel/view.js
@@ -75,7 +75,7 @@ export class LayerPanel extends React.Component {
};
async _loadLeftJoinFields() {
- if (!this.props.selectedLayer || !this.props.selectedLayer.isJoinable()) {
+ if (!this.props.selectedLayer || !this.props.selectedLayer.showJoinEditor()) {
return;
}
@@ -120,7 +120,7 @@ export class LayerPanel extends React.Component {
}
_renderJoinSection() {
- if (!this.props.selectedLayer.isJoinable()) {
+ if (!this.props.selectedLayer.showJoinEditor()) {
return null;
}
@@ -128,6 +128,7 @@ export class LayerPanel extends React.Component {
diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/view.test.js b/x-pack/plugins/maps/public/connected_components/layer_panel/view.test.js
index 99893c1bc5bee..33ca80b00c451 100644
--- a/x-pack/plugins/maps/public/connected_components/layer_panel/view.test.js
+++ b/x-pack/plugins/maps/public/connected_components/layer_panel/view.test.js
@@ -55,7 +55,7 @@ const mockLayer = {
getImmutableSourceProperties: () => {
return [{ label: 'source prop1', value: 'you get one chance to set me' }];
},
- isJoinable: () => {
+ showJoinEditor: () => {
return true;
},
supportsElasticsearchFilters: () => {
From 94ef03dbd3ab57426fc04bbf0d6c11a8e12e11ac Mon Sep 17 00:00:00 2001
From: Jonathan Budzenski
Date: Mon, 13 Jul 2020 10:56:25 -0500
Subject: [PATCH 14/40] Move kibana-keystore from data/ to config/ (#57856)
* Move kibana-keystore from data/ to config/
This is a breaking change to move the location of kibana-keystore.
Keystores in other stack products live in the config directory, so this
updates our current path to be consistent.
Closes #25746
* add breaking changes
* update comment
* wip
* fix docs
* read from both keystore locations, write priority to non-deprecated
* note data directory fallback
* add tests for get_keystore
Co-authored-by: Elastic Machine
---
docs/migration/migrate_8_0.asciidoc | 7 +++-
src/cli_keystore/cli_keystore.js | 8 ++--
src/cli_keystore/get_keystore.js | 40 +++++++++++++++++++
src/cli_keystore/get_keystore.test.js | 57 +++++++++++++++++++++++++++
src/core/server/path/index.test.ts | 7 +++-
src/core/server/path/index.ts | 15 ++++++-
6 files changed, 125 insertions(+), 9 deletions(-)
create mode 100644 src/cli_keystore/get_keystore.js
create mode 100644 src/cli_keystore/get_keystore.test.js
diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc
index 82798e948822a..b80503750a26e 100644
--- a/docs/migration/migrate_8_0.asciidoc
+++ b/docs/migration/migrate_8_0.asciidoc
@@ -115,12 +115,17 @@ URL that it derived from the actual server address and `xpack.security.public` s
*Impact:* Any workflow that involved manually clearing generated bundles will have to be updated with the new path.
+[float]]
+=== kibana.keystore has moved from the `data` folder to the `config` folder
+*Details:* By default, kibana.keystore has moved from the configured `path.data` folder to `/config` for archive distributions
+and `/etc/kibana` for package distributions. If a pre-existing keystore exists in the data directory that path will continue to be used.
+
[float]
[[breaking_80_user_role_changes]]
=== User role changes
[float]
-==== `kibana_user` role has been removed and `kibana_admin` has been added.
+=== `kibana_user` role has been removed and `kibana_admin` has been added.
*Details:* The `kibana_user` role has been removed and `kibana_admin` has been added to better
reflect its intended use. This role continues to grant all access to every
diff --git a/src/cli_keystore/cli_keystore.js b/src/cli_keystore/cli_keystore.js
index e1561b343ef39..d12c80b361c92 100644
--- a/src/cli_keystore/cli_keystore.js
+++ b/src/cli_keystore/cli_keystore.js
@@ -18,20 +18,16 @@
*/
import _ from 'lodash';
-import { join } from 'path';
import { pkg } from '../core/server/utils';
import Command from '../cli/command';
-import { getDataPath } from '../core/server/path';
import { Keystore } from '../legacy/server/keystore';
-const path = join(getDataPath(), 'kibana.keystore');
-const keystore = new Keystore(path);
-
import { createCli } from './create';
import { listCli } from './list';
import { addCli } from './add';
import { removeCli } from './remove';
+import { getKeystore } from './get_keystore';
const argv = process.env.kbnWorkerArgv
? JSON.parse(process.env.kbnWorkerArgv)
@@ -42,6 +38,8 @@ program
.version(pkg.version)
.description('A tool for managing settings stored in the Kibana keystore');
+const keystore = new Keystore(getKeystore());
+
createCli(program, keystore);
listCli(program, keystore);
addCli(program, keystore);
diff --git a/src/cli_keystore/get_keystore.js b/src/cli_keystore/get_keystore.js
new file mode 100644
index 0000000000000..c8ff2555563ad
--- /dev/null
+++ b/src/cli_keystore/get_keystore.js
@@ -0,0 +1,40 @@
+/*
+ * 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 { existsSync } from 'fs';
+import { join } from 'path';
+
+import Logger from '../cli_plugin/lib/logger';
+import { getConfigDirectory, getDataPath } from '../core/server/path';
+
+export function getKeystore() {
+ const configKeystore = join(getConfigDirectory(), 'kibana.keystore');
+ const dataKeystore = join(getDataPath(), 'kibana.keystore');
+ let keystorePath = null;
+ if (existsSync(dataKeystore)) {
+ const logger = new Logger();
+ logger.log(
+ `kibana.keystore located in the data folder is deprecated. Future versions will use the config folder.`
+ );
+ keystorePath = dataKeystore;
+ } else {
+ keystorePath = configKeystore;
+ }
+ return keystorePath;
+}
diff --git a/src/cli_keystore/get_keystore.test.js b/src/cli_keystore/get_keystore.test.js
new file mode 100644
index 0000000000000..88102b8f51d57
--- /dev/null
+++ b/src/cli_keystore/get_keystore.test.js
@@ -0,0 +1,57 @@
+/*
+ * 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 { getKeystore } from './get_keystore';
+import Logger from '../cli_plugin/lib/logger';
+import fs from 'fs';
+import sinon from 'sinon';
+
+describe('get_keystore', () => {
+ const sandbox = sinon.createSandbox();
+
+ beforeEach(() => {
+ sandbox.stub(Logger.prototype, 'log');
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ });
+
+ it('uses the config directory if there is no pre-existing keystore', () => {
+ sandbox.stub(fs, 'existsSync').returns(false);
+ expect(getKeystore()).toContain('config');
+ expect(getKeystore()).not.toContain('data');
+ });
+
+ it('uses the data directory if there is a pre-existing keystore in the data directory', () => {
+ sandbox.stub(fs, 'existsSync').returns(true);
+ expect(getKeystore()).toContain('data');
+ expect(getKeystore()).not.toContain('config');
+ });
+
+ it('logs a deprecation warning if the data directory is used', () => {
+ sandbox.stub(fs, 'existsSync').returns(true);
+ getKeystore();
+ sandbox.assert.calledOnce(Logger.prototype.log);
+ sandbox.assert.calledWith(
+ Logger.prototype.log,
+ 'kibana.keystore located in the data folder is deprecated. Future versions will use the config folder.'
+ );
+ });
+});
diff --git a/src/core/server/path/index.test.ts b/src/core/server/path/index.test.ts
index 048622e1f7eab..522e100d85e5d 100644
--- a/src/core/server/path/index.test.ts
+++ b/src/core/server/path/index.test.ts
@@ -18,7 +18,7 @@
*/
import { accessSync, constants } from 'fs';
-import { getConfigPath, getDataPath } from './';
+import { getConfigPath, getDataPath, getConfigDirectory } from './';
describe('Default path finder', () => {
it('should find a kibana.yml', () => {
@@ -30,4 +30,9 @@ describe('Default path finder', () => {
const dataPath = getDataPath();
expect(() => accessSync(dataPath, constants.R_OK)).not.toThrow();
});
+
+ it('should find a config directory', () => {
+ const configDirectory = getConfigDirectory();
+ expect(() => accessSync(configDirectory, constants.R_OK)).not.toThrow();
+ });
});
diff --git a/src/core/server/path/index.ts b/src/core/server/path/index.ts
index 2e05e3856bd4c..1bb650518c47a 100644
--- a/src/core/server/path/index.ts
+++ b/src/core/server/path/index.ts
@@ -30,6 +30,10 @@ const CONFIG_PATHS = [
fromRoot('config/kibana.yml'),
].filter(isString);
+const CONFIG_DIRECTORIES = [process.env.KIBANA_PATH_CONF, fromRoot('config'), '/etc/kibana'].filter(
+ isString
+);
+
const DATA_PATHS = [
process.env.DATA_PATH, // deprecated
fromRoot('data'),
@@ -49,12 +53,19 @@ function findFile(paths: string[]) {
}
/**
- * Get the path where the config files are stored
+ * Get the path of kibana.yml
* @internal
*/
export const getConfigPath = () => findFile(CONFIG_PATHS);
+
+/**
+ * Get the directory containing configuration files
+ * @internal
+ */
+export const getConfigDirectory = () => findFile(CONFIG_DIRECTORIES);
+
/**
- * Get the path where the data can be stored
+ * Get the directory containing runtime data
* @internal
*/
export const getDataPath = () => findFile(DATA_PATHS);
From ced0023ef943fb2e20c5a53e28f1333bf9cb2dee Mon Sep 17 00:00:00 2001
From: Madison Caldwell
Date: Mon, 13 Jul 2020 12:02:29 -0400
Subject: [PATCH 15/40] Mapping adjustments (#71449)
---
.../server/endpoint/lib/artifacts/common.ts | 4 ++--
.../endpoint/lib/artifacts/saved_object_mappings.ts | 5 ++---
.../endpoint/artifacts/api_feature/data.json | 12 ++++++------
3 files changed, 10 insertions(+), 11 deletions(-)
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts
index 71d14eb1226d5..77a5e85b14199 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts
@@ -7,13 +7,13 @@ import { Logger } from 'src/core/server';
export const ArtifactConstants = {
GLOBAL_ALLOWLIST_NAME: 'endpoint-exceptionlist',
- SAVED_OBJECT_TYPE: 'endpoint:user-artifact:v2',
+ SAVED_OBJECT_TYPE: 'endpoint:user-artifact',
SUPPORTED_OPERATING_SYSTEMS: ['linux', 'macos', 'windows'],
SCHEMA_VERSION: 'v1',
};
export const ManifestConstants = {
- SAVED_OBJECT_TYPE: 'endpoint:user-artifact-manifest:v2',
+ SAVED_OBJECT_TYPE: 'endpoint:user-artifact-manifest',
SCHEMA_VERSION: 'v1',
INITIAL_VERSION: 'WzAsMF0=',
};
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/saved_object_mappings.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/saved_object_mappings.ts
index 89e974a3d5fd3..0fb433df95de3 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/saved_object_mappings.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/saved_object_mappings.ts
@@ -45,7 +45,6 @@ export const exceptionsArtifactSavedObjectMappings: SavedObjectsType['mappings']
},
body: {
type: 'binary',
- index: false,
},
},
};
@@ -66,14 +65,14 @@ export const manifestSavedObjectMappings: SavedObjectsType['mappings'] = {
export const exceptionsArtifactType: SavedObjectsType = {
name: exceptionsArtifactSavedObjectType,
- hidden: false, // TODO: should these be hidden?
+ hidden: false,
namespaceType: 'agnostic',
mappings: exceptionsArtifactSavedObjectMappings,
};
export const manifestType: SavedObjectsType = {
name: manifestSavedObjectType,
- hidden: false, // TODO: should these be hidden?
+ hidden: false,
namespaceType: 'agnostic',
mappings: manifestSavedObjectMappings,
};
diff --git a/x-pack/test/functional/es_archives/endpoint/artifacts/api_feature/data.json b/x-pack/test/functional/es_archives/endpoint/artifacts/api_feature/data.json
index bd1010240f86c..ab476660e3ffc 100644
--- a/x-pack/test/functional/es_archives/endpoint/artifacts/api_feature/data.json
+++ b/x-pack/test/functional/es_archives/endpoint/artifacts/api_feature/data.json
@@ -1,12 +1,12 @@
{
"type": "doc",
"value": {
- "id": "endpoint:user-artifact:v2:endpoint-exceptionlist-linux-v1-d2a9c760005b08d43394e59a8701ae75c80881934ccf15a006944452b80f7f9f",
+ "id": "endpoint:user-artifact:endpoint-exceptionlist-linux-v1-d2a9c760005b08d43394e59a8701ae75c80881934ccf15a006944452b80f7f9f",
"index": ".kibana",
"source": {
"references": [
],
- "endpoint:user-artifact:v2": {
+ "endpoint:user-artifact": {
"body": "eJylkM8KwjAMxl9Fci59gN29iicvMqR02QjUbiSpKGPvbiw6ETwpuX1/fh9kBszKhALNcQa9TQgNCJ2nhOA+vJ4wdWaGqJSHPY8RRXxPCb3QkJEtP07IQUe2GOWYSoedqU8qXq16ikGqeAmpPNRtCqIU3WbnDx4WN38d/WvhQqmCXzDlIlojP9CsjLC0bqWtHwhaGN/1jHVkae3u+6N6Sg==",
"created": 1593016187465,
"compressionAlgorithm": "zlib",
@@ -17,7 +17,7 @@
"decodedSha256": "d2a9c760005b08d43394e59a8701ae75c80881934ccf15a006944452b80f7f9f",
"decodedSize": 358
},
- "type": "endpoint:user-artifact:v2",
+ "type": "endpoint:user-artifact",
"updated_at": "2020-06-24T16:29:47.584Z"
}
}
@@ -26,12 +26,12 @@
{
"type": "doc",
"value": {
- "id": "endpoint:user-artifact-manifest:v2:endpoint-manifest-v1",
+ "id": "endpoint:user-artifact-manifest:endpoint-manifest-v1",
"index": ".kibana",
"source": {
"references": [
],
- "endpoint:user-artifact-manifest:v2": {
+ "endpoint:user-artifact-manifest": {
"created": 1593183699663,
"ids": [
"endpoint-exceptionlist-linux-v1-d2a9c760005b08d43394e59a8701ae75c80881934ccf15a006944452b80f7f9f",
@@ -39,7 +39,7 @@
"endpoint-exceptionlist-windows-v1-d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658"
]
},
- "type": "endpoint:user-artifact-manifest:v2",
+ "type": "endpoint:user-artifact-manifest",
"updated_at": "2020-06-26T15:01:39.704Z"
}
}
From eac0f8d98d5ec8dfcb0ae3d6a6176a9640a5a9ad Mon Sep 17 00:00:00 2001
From: Pierre Gayvallet
Date: Mon, 13 Jul 2020 18:03:49 +0200
Subject: [PATCH 16/40] preserve 401 errors from legacy es client (#71234)
* preserve 401 errors from legacy es client
* use exact import to resolve mocked import issue
---
.../legacy/cluster_client.test.ts | 4 +--
.../core_service.test.mocks.ts | 5 ++-
.../integration_tests/core_services.test.ts | 34 +++++++++++++++++--
src/core/server/http/router/router.ts | 5 +++
4 files changed, 41 insertions(+), 7 deletions(-)
diff --git a/src/core/server/elasticsearch/legacy/cluster_client.test.ts b/src/core/server/elasticsearch/legacy/cluster_client.test.ts
index 2f0f80728c707..fd57d06e61eee 100644
--- a/src/core/server/elasticsearch/legacy/cluster_client.test.ts
+++ b/src/core/server/elasticsearch/legacy/cluster_client.test.ts
@@ -130,7 +130,7 @@ describe('#callAsInternalUser', () => {
expect(mockEsClientInstance.security.authenticate).toHaveBeenLastCalledWith(mockParams);
});
- test('does not wrap errors if `wrap401Errors` is not set', async () => {
+ test('does not wrap errors if `wrap401Errors` is set to `false`', async () => {
const mockError = { message: 'some error' };
mockEsClientInstance.ping.mockRejectedValue(mockError);
@@ -146,7 +146,7 @@ describe('#callAsInternalUser', () => {
).rejects.toBe(mockAuthenticationError);
});
- test('wraps only 401 errors by default or when `wrap401Errors` is set', async () => {
+ test('wraps 401 errors when `wrap401Errors` is set to `true` or unspecified', async () => {
const mockError = { message: 'some error' };
mockEsClientInstance.ping.mockRejectedValue(mockError);
diff --git a/src/core/server/http/integration_tests/core_service.test.mocks.ts b/src/core/server/http/integration_tests/core_service.test.mocks.ts
index f7ebd18b9c488..c23724b7d332f 100644
--- a/src/core/server/http/integration_tests/core_service.test.mocks.ts
+++ b/src/core/server/http/integration_tests/core_service.test.mocks.ts
@@ -19,10 +19,9 @@
import { elasticsearchServiceMock } from '../../elasticsearch/elasticsearch_service.mock';
export const clusterClientMock = jest.fn();
+export const clusterClientInstanceMock = elasticsearchServiceMock.createLegacyScopedClusterClient();
jest.doMock('../../elasticsearch/legacy/scoped_cluster_client', () => ({
- LegacyScopedClusterClient: clusterClientMock.mockImplementation(function () {
- return elasticsearchServiceMock.createLegacyScopedClusterClient();
- }),
+ LegacyScopedClusterClient: clusterClientMock.mockImplementation(() => clusterClientInstanceMock),
}));
jest.doMock('elasticsearch', () => {
diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts
index ba39effa77016..0ee53a04d9f87 100644
--- a/src/core/server/http/integration_tests/core_services.test.ts
+++ b/src/core/server/http/integration_tests/core_services.test.ts
@@ -16,9 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
+
+import { clusterClientMock, clusterClientInstanceMock } from './core_service.test.mocks';
+
import Boom from 'boom';
import { Request } from 'hapi';
-import { clusterClientMock } from './core_service.test.mocks';
+import { errors as esErrors } from 'elasticsearch';
+import { LegacyElasticsearchErrorHelpers } from '../../elasticsearch/legacy';
import * as kbnTestServer from '../../../../test_utils/kbn_server';
@@ -352,7 +356,7 @@ describe('http service', () => {
});
});
});
- describe('elasticsearch', () => {
+ describe('legacy elasticsearch client', () => {
let root: ReturnType;
beforeEach(async () => {
root = kbnTestServer.createRoot({ plugins: { initialize: false } });
@@ -410,5 +414,31 @@ describe('http service', () => {
const [, , clientHeaders] = client;
expect(clientHeaders).toEqual({ authorization: authorizationHeader });
});
+
+ it('forwards 401 errors returned from elasticsearch', async () => {
+ const { http } = await root.setup();
+ const { createRouter } = http;
+
+ const authenticationError = LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(
+ new (esErrors.AuthenticationException as any)('Authentication Exception', {
+ body: { error: { header: { 'WWW-Authenticate': 'authenticate header' } } },
+ statusCode: 401,
+ })
+ );
+
+ clusterClientInstanceMock.callAsCurrentUser.mockRejectedValue(authenticationError);
+
+ const router = createRouter('/new-platform');
+ router.get({ path: '/', validate: false }, async (context, req, res) => {
+ await context.core.elasticsearch.legacy.client.callAsCurrentUser('ping');
+ return res.ok();
+ });
+
+ await root.start();
+
+ const response = await kbnTestServer.request.get(root, '/new-platform/').expect(401);
+
+ expect(response.header['www-authenticate']).toEqual('authenticate header');
+ });
});
});
diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts
index 69402a74eda5f..35eec746163ce 100644
--- a/src/core/server/http/router/router.ts
+++ b/src/core/server/http/router/router.ts
@@ -22,6 +22,7 @@ import Boom from 'boom';
import { isConfigSchema } from '@kbn/config-schema';
import { Logger } from '../../logging';
+import { LegacyElasticsearchErrorHelpers } from '../../elasticsearch/legacy/errors';
import { KibanaRequest } from './request';
import { KibanaResponseFactory, kibanaResponseFactory, IKibanaResponse } from './response';
import { RouteConfig, RouteConfigOptions, RouteMethod, validBodyOutput } from './route';
@@ -263,6 +264,10 @@ export class Router implements IRouter {
return hapiResponseAdapter.handle(kibanaResponse);
} catch (e) {
this.log.error(e);
+ // forward 401 (boom) error from ES
+ if (LegacyElasticsearchErrorHelpers.isNotAuthorizedError(e)) {
+ return e;
+ }
return hapiResponseAdapter.toInternalError();
}
}
From 105afbce3d0d0264ea7de4c901e4dd41f392e89e Mon Sep 17 00:00:00 2001
From: Alison Goryachev
Date: Mon, 13 Jul 2020 12:10:01 -0400
Subject: [PATCH 17/40] [Component templates] Address feedback (#70912)
---
.../component_template_serialization.test.ts | 2 +
.../lib/component_template_serialization.ts | 6 +-
.../index_management/common/lib/index.ts | 2 +-
.../common/types/component_templates.ts | 2 +
.../component_template_create.test.tsx | 2 +-
.../component_template_details.test.ts | 4 +-
.../component_template_edit.test.tsx | 2 +-
.../component_template_list.test.ts | 2 +
.../helpers/setup_environment.tsx | 2 +
.../component_template_details.tsx | 38 +++++++++--
.../tab_summary.tsx | 48 ++++++++++++-
.../component_template_list.tsx | 42 +++++++++---
.../component_template_list/empty_prompt.tsx | 6 +-
.../component_template_list/table.tsx | 51 +++++++++-----
.../component_template_form.tsx | 67 +++++++++++++------
.../steps/step_logistics.tsx | 8 +--
.../steps/step_review.tsx | 14 ++--
.../component_templates_context.tsx | 25 ++++++-
.../component_templates/shared_imports.ts | 2 +
.../public/application/index.tsx | 3 +-
.../routes/api/component_templates/get.ts | 4 +-
.../component_templates/schema_validation.ts | 1 +
.../index_management/component_templates.ts | 7 ++
23 files changed, 254 insertions(+), 86 deletions(-)
diff --git a/x-pack/plugins/index_management/common/lib/component_template_serialization.test.ts b/x-pack/plugins/index_management/common/lib/component_template_serialization.test.ts
index 83682f45918e3..16c45991d1f32 100644
--- a/x-pack/plugins/index_management/common/lib/component_template_serialization.test.ts
+++ b/x-pack/plugins/index_management/common/lib/component_template_serialization.test.ts
@@ -92,6 +92,7 @@ describe('Component template serialization', () => {
},
_kbnMeta: {
usedBy: ['my_index_template'],
+ isManaged: false,
},
});
});
@@ -105,6 +106,7 @@ describe('Component template serialization', () => {
version: 1,
_kbnMeta: {
usedBy: [],
+ isManaged: false,
},
_meta: {
serialization: {
diff --git a/x-pack/plugins/index_management/common/lib/component_template_serialization.ts b/x-pack/plugins/index_management/common/lib/component_template_serialization.ts
index 672b8140f79fb..3a1c2c1ca55b2 100644
--- a/x-pack/plugins/index_management/common/lib/component_template_serialization.ts
+++ b/x-pack/plugins/index_management/common/lib/component_template_serialization.ts
@@ -60,24 +60,26 @@ export function deserializeComponentTemplate(
_meta,
_kbnMeta: {
usedBy: indexTemplatesToUsedBy[name] || [],
+ isManaged: Boolean(_meta?.managed === true),
},
};
return deserializedComponentTemplate;
}
-export function deserializeComponenTemplateList(
+export function deserializeComponentTemplateList(
componentTemplateEs: ComponentTemplateFromEs,
indexTemplatesEs: TemplateFromEs[]
) {
const { name, component_template: componentTemplate } = componentTemplateEs;
- const { template } = componentTemplate;
+ const { template, _meta } = componentTemplate;
const indexTemplatesToUsedBy = getIndexTemplatesToUsedBy(indexTemplatesEs);
const componentTemplateListItem: ComponentTemplateListItem = {
name,
usedBy: indexTemplatesToUsedBy[name] || [],
+ isManaged: Boolean(_meta?.managed === true),
hasSettings: hasEntries(template.settings),
hasMappings: hasEntries(template.mappings),
hasAliases: hasEntries(template.aliases),
diff --git a/x-pack/plugins/index_management/common/lib/index.ts b/x-pack/plugins/index_management/common/lib/index.ts
index f39cc063ba731..9e87e87b0eee0 100644
--- a/x-pack/plugins/index_management/common/lib/index.ts
+++ b/x-pack/plugins/index_management/common/lib/index.ts
@@ -19,6 +19,6 @@ export { getTemplateParameter } from './utils';
export {
deserializeComponentTemplate,
- deserializeComponenTemplateList,
+ deserializeComponentTemplateList,
serializeComponentTemplate,
} from './component_template_serialization';
diff --git a/x-pack/plugins/index_management/common/types/component_templates.ts b/x-pack/plugins/index_management/common/types/component_templates.ts
index bc7ebdc2753dd..c8dec40d061bd 100644
--- a/x-pack/plugins/index_management/common/types/component_templates.ts
+++ b/x-pack/plugins/index_management/common/types/component_templates.ts
@@ -22,6 +22,7 @@ export interface ComponentTemplateDeserialized extends ComponentTemplateSerializ
name: string;
_kbnMeta: {
usedBy: string[];
+ isManaged: boolean;
};
}
@@ -36,4 +37,5 @@ export interface ComponentTemplateListItem {
hasMappings: boolean;
hasAliases: boolean;
hasSettings: boolean;
+ isManaged: boolean;
}
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx
index 75eb419d56a5c..4462a42758878 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx
@@ -185,7 +185,7 @@ describe('', () => {
},
aliases: ALIASES,
},
- _kbnMeta: { usedBy: [] },
+ _kbnMeta: { usedBy: [], isManaged: false },
};
expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected);
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts
index 7c17dde119c42..3d496d68cc66e 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts
@@ -26,13 +26,13 @@ const COMPONENT_TEMPLATE: ComponentTemplateDeserialized = {
},
version: 1,
_meta: { description: 'component template test' },
- _kbnMeta: { usedBy: ['template_1'] },
+ _kbnMeta: { usedBy: ['template_1'], isManaged: false },
};
const COMPONENT_TEMPLATE_ONLY_REQUIRED_FIELDS: ComponentTemplateDeserialized = {
name: 'comp-base',
template: {},
- _kbnMeta: { usedBy: [] },
+ _kbnMeta: { usedBy: [], isManaged: false },
};
describe('', () => {
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx
index 115fdf032da8f..114cafe9defde 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx
@@ -52,7 +52,7 @@ describe('', () => {
template: {
settings: { number_of_shards: 1 },
},
- _kbnMeta: { usedBy: [] },
+ _kbnMeta: { usedBy: [], isManaged: false },
};
beforeEach(async () => {
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts
index 6f09e51255f3b..bd6ac27375836 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts
@@ -42,6 +42,7 @@ describe('', () => {
hasAliases: true,
hasSettings: true,
usedBy: [],
+ isManaged: false,
};
const componentTemplate2: ComponentTemplateListItem = {
@@ -50,6 +51,7 @@ describe('', () => {
hasAliases: true,
hasSettings: true,
usedBy: ['test_index_template_1'],
+ isManaged: false,
};
const componentTemplates = [componentTemplate1, componentTemplate2];
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx
index 70634a226c67b..7e460d3855cb0 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx
@@ -12,6 +12,7 @@ import { HttpSetup } from 'kibana/public';
import {
notificationServiceMock,
docLinksServiceMock,
+ applicationServiceMock,
} from '../../../../../../../../../../src/core/public/mocks';
import { ComponentTemplatesProvider } from '../../../component_templates_context';
@@ -28,6 +29,7 @@ const appDependencies = {
docLinks: docLinksServiceMock.createStartContract(),
toasts: notificationServiceMock.createSetupContract().toasts,
setBreadcrumbs: () => {},
+ getUrlForApp: applicationServiceMock.createStartContract().getUrlForApp,
};
export const setupEnvironment = () => {
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx
index f94c5c38f23dd..60f1fff3cc9de 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx
@@ -6,6 +6,7 @@
import React, { useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
+
import {
EuiFlyout,
EuiFlyoutHeader,
@@ -17,6 +18,7 @@ import {
EuiButtonEmpty,
EuiSpacer,
EuiCallOut,
+ EuiBadge,
} from '@elastic/eui';
import { SectionLoading, TabSettings, TabAliases, TabMappings } from '../shared_imports';
@@ -29,14 +31,15 @@ import { attemptToDecodeURI } from '../lib';
interface Props {
componentTemplateName: string;
onClose: () => void;
- showFooter?: boolean;
actions?: ManageAction[];
+ showSummaryCallToAction?: boolean;
}
export const ComponentTemplateDetailsFlyout: React.FunctionComponent = ({
componentTemplateName,
onClose,
actions,
+ showSummaryCallToAction,
}) => {
const { api } = useComponentTemplatesContext();
@@ -81,7 +84,12 @@ export const ComponentTemplateDetailsFlyout: React.FunctionComponent = ({
} = componentTemplateDetails;
const tabToComponentMap: Record = {
- summary: ,
+ summary: (
+
+ ),
settings: ,
mappings: ,
aliases: ,
@@ -109,11 +117,27 @@ export const ComponentTemplateDetailsFlyout: React.FunctionComponent = ({
maxWidth={500}
>
-
-
- {decodedComponentTemplateName}
-
-
+
+
+
+
+ {decodedComponentTemplateName}
+
+
+
+
+ {componentTemplateDetails?._kbnMeta.isManaged ? (
+
+ {' '}
+
+
+
+
+ ) : null}
+
{content}
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx
index 80f28f23c9f91..8d054b97cb4f6 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx
@@ -6,6 +6,7 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
+
import {
EuiDescriptionList,
EuiDescriptionListTitle,
@@ -14,15 +15,23 @@ import {
EuiTitle,
EuiCallOut,
EuiSpacer,
+ EuiLink,
} from '@elastic/eui';
import { ComponentTemplateDeserialized } from '../shared_imports';
+import { useComponentTemplatesContext } from '../component_templates_context';
interface Props {
componentTemplateDetails: ComponentTemplateDeserialized;
+ showCallToAction?: boolean;
}
-export const TabSummary: React.FunctionComponent = ({ componentTemplateDetails }) => {
+export const TabSummary: React.FunctionComponent = ({
+ componentTemplateDetails,
+ showCallToAction,
+}) => {
+ const { getUrlForApp } = useComponentTemplatesContext();
+
const { version, _meta, _kbnMeta } = componentTemplateDetails;
const { usedBy } = _kbnMeta;
@@ -43,7 +52,42 @@ export const TabSummary: React.FunctionComponent = ({ componentTemplateDe
iconType="pin"
data-test-subj="notInUseCallout"
size="s"
- />
+ >
+ {showCallToAction && (
+
+
+
+
+ ),
+ editLink: (
+
+
+
+ ),
+ }}
+ />
+
+ )}
+
>
)}
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx
index d356eabc7997d..efc8b649ef872 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx
@@ -9,6 +9,7 @@ import { RouteComponentProps } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { ScopedHistory } from 'kibana/public';
+import { EuiLink, EuiText, EuiSpacer } from '@elastic/eui';
import { SectionLoading, ComponentTemplateDeserialized } from '../shared_imports';
import { UIM_COMPONENT_TEMPLATE_LIST_LOAD } from '../constants';
@@ -29,7 +30,7 @@ export const ComponentTemplateList: React.FunctionComponent = ({
componentTemplateName,
history,
}) => {
- const { api, trackMetric } = useComponentTemplatesContext();
+ const { api, trackMetric, documentation } = useComponentTemplatesContext();
const { data, isLoading, error, sendRequest } = api.useLoadComponentTemplates();
@@ -65,20 +66,40 @@ export const ComponentTemplateList: React.FunctionComponent = ({
);
} else if (data?.length) {
content = (
-
+ <>
+
+
+ {i18n.translate('xpack.idxMgmt.componentTemplates.list.learnMoreLinkText', {
+ defaultMessage: 'Learn more.',
+ })}
+
+ ),
+ }}
+ />
+
+
+
+
+
+ >
);
} else if (data && data.length === 0) {
content = ;
@@ -111,6 +132,7 @@ export const ComponentTemplateList: React.FunctionComponent = ({
= ({ history }) => {
{i18n.translate('xpack.idxMgmt.home.componentTemplates.emptyPromptDocumentionLink', {
- defaultMessage: 'Learn more',
+ defaultMessage: 'Learn more.',
})}
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx
index 089c2f889e726..fc86609f1217d 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx
@@ -13,11 +13,11 @@ import {
EuiTextColor,
EuiIcon,
EuiLink,
+ EuiBadge,
} from '@elastic/eui';
import { ScopedHistory } from 'kibana/public';
-import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public';
-import { ComponentTemplateListItem } from '../shared_imports';
+import { ComponentTemplateListItem, reactRouterNavigate } from '../shared_imports';
import { UIM_COMPONENT_TEMPLATE_DETAILS } from '../constants';
import { useComponentTemplatesContext } from '../component_templates_context';
@@ -105,6 +105,13 @@ export const ComponentTable: FunctionComponent = ({
incremental: true,
},
filters: [
+ {
+ type: 'is',
+ field: 'isManaged',
+ name: i18n.translate('xpack.idxMgmt.componentTemplatesList.table.isManagedFilterLabel', {
+ defaultMessage: 'Managed',
+ }),
+ },
{
type: 'field_value_toggle_group',
field: 'usedBy.length',
@@ -144,26 +151,38 @@ export const ComponentTable: FunctionComponent = ({
defaultMessage: 'Name',
}),
sortable: true,
- render: (name: string) => (
- /* eslint-disable-next-line @elastic/eui/href-or-on-click */
- trackMetric('click', UIM_COMPONENT_TEMPLATE_DETAILS)
+ width: '20%',
+ render: (name: string, item: ComponentTemplateListItem) => (
+ <>
+ trackMetric('click', UIM_COMPONENT_TEMPLATE_DETAILS)
+ )}
+ data-test-subj="templateDetailsLink"
+ >
+ {name}
+
+ {item.isManaged && (
+ <>
+
+
+ {i18n.translate('xpack.idxMgmt.componentTemplatesList.table.managedBadgeLabel', {
+ defaultMessage: 'Managed',
+ })}
+
+ >
)}
- data-test-subj="templateDetailsLink"
- >
- {name}
-
+ >
),
},
{
field: 'usedBy',
name: i18n.translate('xpack.idxMgmt.componentTemplatesList.table.isInUseColumnTitle', {
- defaultMessage: 'Index templates',
+ defaultMessage: 'Usage count',
}),
sortable: true,
render: (usedBy: string[]) => {
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/component_template_form.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/component_template_form.tsx
index 6e35fbad31d4e..134b8b5eda93d 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/component_template_form.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/component_template_form.tsx
@@ -74,14 +74,11 @@ const wizardSections: { [id: string]: { id: WizardSection; label: string } } = {
export const ComponentTemplateForm = ({
defaultValue = {
name: '',
- template: {
- settings: {},
- mappings: {},
- aliases: {},
- },
+ template: {},
_meta: {},
_kbnMeta: {
usedBy: [],
+ isManaged: false,
},
},
isEditing,
@@ -137,23 +134,49 @@ export const ComponentTemplateForm = ({
>
) : null;
- const buildComponentTemplateObject = (initialTemplate: ComponentTemplateDeserialized) => (
- wizardData: WizardContent
- ): ComponentTemplateDeserialized => {
- const componentTemplate = {
- ...initialTemplate,
- name: wizardData.logistics.name,
- version: wizardData.logistics.version,
- _meta: wizardData.logistics._meta,
- template: {
- settings: wizardData.settings,
- mappings: wizardData.mappings,
- aliases: wizardData.aliases,
- },
- };
- return componentTemplate;
+ /**
+ * If no mappings, settings or aliases are defined, it is better to not send an empty
+ * object for those values.
+ * @param componentTemplate The component template object to clean up
+ */
+ const cleanupComponentTemplateObject = (componentTemplate: ComponentTemplateDeserialized) => {
+ const outputTemplate = { ...componentTemplate };
+
+ if (outputTemplate.template.settings === undefined) {
+ delete outputTemplate.template.settings;
+ }
+
+ if (outputTemplate.template.mappings === undefined) {
+ delete outputTemplate.template.mappings;
+ }
+
+ if (outputTemplate.template.aliases === undefined) {
+ delete outputTemplate.template.aliases;
+ }
+
+ return outputTemplate;
};
+ const buildComponentTemplateObject = useCallback(
+ (initialTemplate: ComponentTemplateDeserialized) => (
+ wizardData: WizardContent
+ ): ComponentTemplateDeserialized => {
+ const outputComponentTemplate = {
+ ...initialTemplate,
+ name: wizardData.logistics.name,
+ version: wizardData.logistics.version,
+ _meta: wizardData.logistics._meta,
+ template: {
+ settings: wizardData.settings,
+ mappings: wizardData.mappings,
+ aliases: wizardData.aliases,
+ },
+ };
+ return cleanupComponentTemplateObject(outputComponentTemplate);
+ },
+ []
+ );
+
const onSaveComponentTemplate = useCallback(
async (wizardData: WizardContent) => {
const componentTemplate = buildComponentTemplateObject(defaultValue)(wizardData);
@@ -161,13 +184,13 @@ export const ComponentTemplateForm = ({
// This will strip an empty string if "version" is not set, as well as an empty "_meta" object
onSave(
stripEmptyFields(componentTemplate, {
- types: ['string', 'object'],
+ types: ['string'],
}) as ComponentTemplateDeserialized
);
clearSaveError();
},
- [defaultValue, onSave, clearSaveError]
+ [buildComponentTemplateObject, defaultValue, onSave, clearSaveError]
);
return (
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx
index 18988fa125a06..c48a23226a371 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx
@@ -117,7 +117,7 @@ export const StepLogistics: React.FunctionComponent = React.memo(
description={
}
>
@@ -141,7 +141,7 @@ export const StepLogistics: React.FunctionComponent = React.memo(
description={
}
>
@@ -165,7 +165,7 @@ export const StepLogistics: React.FunctionComponent = React.memo(
<>
= React.memo(
{i18n.translate(
'xpack.idxMgmt.componentTemplateForm.stepLogistics.metaDocumentionLink',
{
- defaultMessage: 'Learn more',
+ defaultMessage: 'Learn more.',
}
)}
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx
index ce85854dc79ab..67246f2e10c3b 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx
@@ -52,16 +52,12 @@ export const StepReview: React.FunctionComponent = React.memo(({ componen
const serializedComponentTemplate = serializeComponentTemplate(
stripEmptyFields(componentTemplate, {
- types: ['string', 'object'],
+ types: ['string'],
}) as ComponentTemplateDeserialized
);
const {
- template: {
- mappings: serializedMappings,
- settings: serializedSettings,
- aliases: serializedAliases,
- },
+ template: serializedTemplate,
_meta: serializedMeta,
version: serializedVersion,
} = serializedComponentTemplate;
@@ -94,7 +90,7 @@ export const StepReview: React.FunctionComponent = React.memo(({ componen
/>
- {getDescriptionText(serializedSettings)}
+ {getDescriptionText(serializedTemplate?.settings)}
{/* Mappings */}
@@ -105,7 +101,7 @@ export const StepReview: React.FunctionComponent = React.memo(({ componen
/>
- {getDescriptionText(serializedMappings)}
+ {getDescriptionText(serializedTemplate?.mappings)}
{/* Aliases */}
@@ -116,7 +112,7 @@ export const StepReview: React.FunctionComponent = React.memo(({ componen
/>
- {getDescriptionText(serializedAliases)}
+ {getDescriptionText(serializedTemplate?.aliases)}
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx
index ce9e28d0feefe..7be0618481a69 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx
@@ -5,7 +5,7 @@
*/
import React, { createContext, useContext } from 'react';
-import { HttpSetup, DocLinksStart, NotificationsSetup } from 'src/core/public';
+import { HttpSetup, DocLinksStart, NotificationsSetup, CoreStart } from 'src/core/public';
import { ManagementAppMountParams } from 'src/plugins/management/public';
import { getApi, getUseRequest, getSendRequest, getDocumentation, getBreadcrumbs } from './lib';
@@ -19,6 +19,7 @@ interface Props {
docLinks: DocLinksStart;
toasts: NotificationsSetup['toasts'];
setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs'];
+ getUrlForApp: CoreStart['application']['getUrlForApp'];
}
interface Context {
@@ -29,6 +30,7 @@ interface Context {
breadcrumbs: ReturnType;
trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void;
toasts: NotificationsSetup['toasts'];
+ getUrlForApp: CoreStart['application']['getUrlForApp'];
}
export const ComponentTemplatesProvider = ({
@@ -38,7 +40,15 @@ export const ComponentTemplatesProvider = ({
value: Props;
children: React.ReactNode;
}) => {
- const { httpClient, apiBasePath, trackMetric, docLinks, toasts, setBreadcrumbs } = value;
+ const {
+ httpClient,
+ apiBasePath,
+ trackMetric,
+ docLinks,
+ toasts,
+ setBreadcrumbs,
+ getUrlForApp,
+ } = value;
const useRequest = getUseRequest(httpClient);
const sendRequest = getSendRequest(httpClient);
@@ -49,7 +59,16 @@ export const ComponentTemplatesProvider = ({
return (
{children}
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts
index 80e222f4f7706..278fadcd90c8b 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts
@@ -62,3 +62,5 @@ export {
} from '../../../../common';
export { serializeComponentTemplate } from '../../../../common/lib';
+
+export { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public';
diff --git a/x-pack/plugins/index_management/public/application/index.tsx b/x-pack/plugins/index_management/public/application/index.tsx
index 7b053a15b26d0..ebc29ac86a17f 100644
--- a/x-pack/plugins/index_management/public/application/index.tsx
+++ b/x-pack/plugins/index_management/public/application/index.tsx
@@ -25,7 +25,7 @@ export const renderApp = (
return () => undefined;
}
- const { i18n, docLinks, notifications } = core;
+ const { i18n, docLinks, notifications, application } = core;
const { Context: I18nContext } = i18n;
const { services, history, setBreadcrumbs } = dependencies;
@@ -36,6 +36,7 @@ export const renderApp = (
docLinks,
toasts: notifications.toasts,
setBreadcrumbs,
+ getUrlForApp: application.getUrlForApp,
};
render(
diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts
index f6f8e7d63d370..16b028887f63c 100644
--- a/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts
+++ b/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts
@@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema';
import {
deserializeComponentTemplate,
- deserializeComponenTemplateList,
+ deserializeComponentTemplateList,
} from '../../../../common/lib';
import { ComponentTemplateFromEs } from '../../../../common';
import { RouteDependencies } from '../../../types';
@@ -36,7 +36,7 @@ export function registerGetAllRoute({ router, license, lib: { isEsError } }: Rou
);
const body = componentTemplates.map((componentTemplate) => {
- const deserializedComponentTemplateListItem = deserializeComponenTemplateList(
+ const deserializedComponentTemplateListItem = deserializeComponentTemplateList(
componentTemplate,
indexTemplates
);
diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts
index a1fc258127229..cfcb428f00501 100644
--- a/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts
+++ b/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts
@@ -16,5 +16,6 @@ export const componentTemplateSchema = schema.object({
_meta: schema.maybe(schema.object({}, { unknowns: 'allow' })),
_kbnMeta: schema.object({
usedBy: schema.arrayOf(schema.string()),
+ isManaged: schema.boolean(),
}),
});
diff --git a/x-pack/test/api_integration/apis/management/index_management/component_templates.ts b/x-pack/test/api_integration/apis/management/index_management/component_templates.ts
index 1a00eaba35aa1..30ec95f208c80 100644
--- a/x-pack/test/api_integration/apis/management/index_management/component_templates.ts
+++ b/x-pack/test/api_integration/apis/management/index_management/component_templates.ts
@@ -78,6 +78,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(testComponentTemplate).to.eql({
name: COMPONENT_NAME,
usedBy: [],
+ isManaged: false,
hasSettings: true,
hasMappings: true,
hasAliases: false,
@@ -96,6 +97,7 @@ export default function ({ getService }: FtrProviderContext) {
...COMPONENT,
_kbnMeta: {
usedBy: [],
+ isManaged: false,
},
});
});
@@ -148,6 +150,7 @@ export default function ({ getService }: FtrProviderContext) {
},
_kbnMeta: {
usedBy: [],
+ isManaged: false,
},
})
.expect(200);
@@ -167,6 +170,7 @@ export default function ({ getService }: FtrProviderContext) {
template: {},
_kbnMeta: {
usedBy: [],
+ isManaged: false,
},
})
.expect(200);
@@ -185,6 +189,7 @@ export default function ({ getService }: FtrProviderContext) {
template: {},
_kbnMeta: {
usedBy: [],
+ isManaged: false,
},
})
.expect(409);
@@ -246,6 +251,7 @@ export default function ({ getService }: FtrProviderContext) {
version: 1,
_kbnMeta: {
usedBy: [],
+ isManaged: false,
},
})
.expect(200);
@@ -267,6 +273,7 @@ export default function ({ getService }: FtrProviderContext) {
version: 1,
_kbnMeta: {
usedBy: [],
+ isManaged: false,
},
})
.expect(404);
From e082719870375a2188d50042e3a92ddff991ea76 Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Mon, 13 Jul 2020 17:12:25 +0100
Subject: [PATCH 18/40] skip flaky suite (#68400)
---
.../apps/saved_objects_management/edit_saved_object.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/test/functional/apps/saved_objects_management/edit_saved_object.ts b/test/functional/apps/saved_objects_management/edit_saved_object.ts
index 2c9200c2f8d93..0e2ff44ff62ef 100644
--- a/test/functional/apps/saved_objects_management/edit_saved_object.ts
+++ b/test/functional/apps/saved_objects_management/edit_saved_object.ts
@@ -66,6 +66,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await button.click();
};
+ // Flaky: https://github.com/elastic/kibana/issues/68400
describe('saved objects edition page', () => {
beforeEach(async () => {
await esArchiver.load('saved_objects_management/edit_saved_object');
From 56794718c7cda41824bf5172fb28930dd06622ce Mon Sep 17 00:00:00 2001
From: Liza Katz
Date: Mon, 13 Jul 2020 19:21:34 +0300
Subject: [PATCH 19/40] Resolve range date filter bugs and improve usability
(#71298)
* improve test stability
* Filter date range improvements
* Make onBlur optional
* i18n
Co-authored-by: Elastic Machine
---
.../lib/filter_editor_utils.test.ts | 18 +++++++++-
.../filter_editor/lib/filter_editor_utils.ts | 5 ++-
.../filter_editor/range_value_input.tsx | 35 ++++++++++---------
.../filter_editor/value_input_type.tsx | 9 +++++
.../translations/translations/ja-JP.json | 1 -
.../translations/translations/zh-CN.json | 1 -
6 files changed, 49 insertions(+), 20 deletions(-)
diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.test.ts b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.test.ts
index 12cdf13caeb55..e2caca7895c42 100644
--- a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.test.ts
+++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.test.ts
@@ -177,11 +177,27 @@ describe('Filter editor utils', () => {
it('should return true for range filter with from/to', () => {
const isValid = isFilterValid(stubIndexPattern, stubFields[0], isBetweenOperator, {
from: 'foo',
- too: 'goo',
+ to: 'goo',
});
expect(isValid).toBe(true);
});
+ it('should return false for date range filter with bad from', () => {
+ const isValid = isFilterValid(stubIndexPattern, stubFields[4], isBetweenOperator, {
+ from: 'foo',
+ to: 'now',
+ });
+ expect(isValid).toBe(false);
+ });
+
+ it('should return false for date range filter with bad to', () => {
+ const isValid = isFilterValid(stubIndexPattern, stubFields[4], isBetweenOperator, {
+ from: '2020-01-01',
+ to: 'mau',
+ });
+ expect(isValid).toBe(false);
+ });
+
it('should return true for exists filter without params', () => {
const isValid = isFilterValid(stubIndexPattern, stubFields[0], existsOperator);
expect(isValid).toBe(true);
diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts
index 114be67e490cf..97a59fa69f458 100644
--- a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts
+++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts
@@ -85,7 +85,10 @@ export function isFilterValid(
if (typeof params !== 'object') {
return false;
}
- return validateParams(params.from, field.type) || validateParams(params.to, field.type);
+ return (
+ (!params.from || validateParams(params.from, field.type)) &&
+ (!params.to || validateParams(params.to, field.type))
+ );
case 'exists':
return true;
default:
diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/range_value_input.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/range_value_input.tsx
index 65b842f0bd4aa..bdfd1014625d8 100644
--- a/src/plugins/data/public/ui/filter_bar/filter_editor/range_value_input.tsx
+++ b/src/plugins/data/public/ui/filter_bar/filter_editor/range_value_input.tsx
@@ -17,8 +17,9 @@
* under the License.
*/
-import { EuiIcon, EuiLink, EuiFormHelpText, EuiFormControlLayoutDelimited } from '@elastic/eui';
-import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
+import moment from 'moment';
+import { EuiFormControlLayoutDelimited } from '@elastic/eui';
+import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { get } from 'lodash';
import React from 'react';
import { useKibana } from '../../../../../kibana_react/public';
@@ -41,8 +42,17 @@ interface Props {
function RangeValueInputUI(props: Props) {
const kibana = useKibana();
- const dataMathDocLink = kibana.services.docLinks!.links.date.dateMath;
const type = props.field ? props.field.type : 'string';
+ const tzConfig = kibana.services.uiSettings!.get('dateFormat:tz');
+
+ const formatDateChange = (value: string | number | boolean) => {
+ if (typeof value !== 'string' && typeof value !== 'number') return value;
+
+ const momentParsedValue = moment(value).tz(tzConfig);
+ if (momentParsedValue.isValid()) return momentParsedValue?.format('YYYY-MM-DDTHH:mm:ss.SSSZ');
+
+ return value;
+ };
const onFromChange = (value: string | number | boolean) => {
if (typeof value !== 'string' && typeof value !== 'number') {
@@ -71,6 +81,9 @@ function RangeValueInputUI(props: Props) {
type={type}
value={props.value ? props.value.from : undefined}
onChange={onFromChange}
+ onBlur={(value) => {
+ onFromChange(formatDateChange(value));
+ }}
placeholder={props.intl.formatMessage({
id: 'data.filter.filterEditor.rangeStartInputPlaceholder',
defaultMessage: 'Start of the range',
@@ -83,6 +96,9 @@ function RangeValueInputUI(props: Props) {
type={type}
value={props.value ? props.value.to : undefined}
onChange={onToChange}
+ onBlur={(value) => {
+ onToChange(formatDateChange(value));
+ }}
placeholder={props.intl.formatMessage({
id: 'data.filter.filterEditor.rangeEndInputPlaceholder',
defaultMessage: 'End of the range',
@@ -90,19 +106,6 @@ function RangeValueInputUI(props: Props) {
/>
}
/>
- {type === 'date' ? (
-
-
- {' '}
-
-
-
- ) : (
- ''
- )}
);
}
diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/value_input_type.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/value_input_type.tsx
index 3737dae1bf9ef..1a165c78d4d79 100644
--- a/src/plugins/data/public/ui/filter_bar/filter_editor/value_input_type.tsx
+++ b/src/plugins/data/public/ui/filter_bar/filter_editor/value_input_type.tsx
@@ -27,6 +27,7 @@ interface Props {
value?: string | number;
type: string;
onChange: (value: string | number | boolean) => void;
+ onBlur?: (value: string | number | boolean) => void;
placeholder: string;
intl: InjectedIntl;
controlOnly?: boolean;
@@ -66,6 +67,7 @@ class ValueInputTypeUI extends Component {
placeholder={this.props.placeholder}
value={value}
onChange={this.onChange}
+ onBlur={this.onBlur}
isInvalid={!isEmpty(value) && !validateParams(value, this.props.type)}
controlOnly={this.props.controlOnly}
className={this.props.className}
@@ -126,6 +128,13 @@ class ValueInputTypeUI extends Component {
const params = event.target.value;
this.props.onChange(params);
};
+
+ private onBlur = (event: React.ChangeEvent) => {
+ if (this.props.onBlur) {
+ const params = event.target.value;
+ this.props.onBlur(params);
+ }
+ };
}
export const ValueInputType = injectI18n(ValueInputTypeUI);
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index e28ef8ff07bdd..4c83fa71a7060 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -641,7 +641,6 @@
"data.filter.filterEditor.cancelButtonLabel": "キャンセル",
"data.filter.filterEditor.createCustomLabelInputLabel": "カスタムラベル",
"data.filter.filterEditor.createCustomLabelSwitchLabel": "カスタムラベルを作成しますか?",
- "data.filter.filterEditor.dateFormatHelpLinkLabel": "対応データフォーマット",
"data.filter.filterEditor.doesNotExistOperatorOptionLabel": "存在しません",
"data.filter.filterEditor.editFilterPopupTitle": "フィルターを編集",
"data.filter.filterEditor.editFilterValuesButtonLabel": "フィルター値を編集",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 1df676ba7cffd..86b2480e3b314 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -641,7 +641,6 @@
"data.filter.filterEditor.cancelButtonLabel": "取消",
"data.filter.filterEditor.createCustomLabelInputLabel": "定制标签",
"data.filter.filterEditor.createCustomLabelSwitchLabel": "创建定制标签?",
- "data.filter.filterEditor.dateFormatHelpLinkLabel": "已接受日期格式",
"data.filter.filterEditor.doesNotExistOperatorOptionLabel": "不存在",
"data.filter.filterEditor.editFilterPopupTitle": "编辑筛选",
"data.filter.filterEditor.editFilterValuesButtonLabel": "编辑筛选值",
From 3a52eaf7ee5e2b2a00fe9c40191528d8ca0ce97e Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Mon, 13 Jul 2020 17:31:08 +0100
Subject: [PATCH 20/40] skip flaky suite (#70928)
---
x-pack/test/functional_embedded/tests/iframe_embedded.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/test/functional_embedded/tests/iframe_embedded.ts b/x-pack/test/functional_embedded/tests/iframe_embedded.ts
index 9b5c9894a9407..f05d70b6cb3e8 100644
--- a/x-pack/test/functional_embedded/tests/iframe_embedded.ts
+++ b/x-pack/test/functional_embedded/tests/iframe_embedded.ts
@@ -14,7 +14,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const config = getService('config');
const testSubjects = getService('testSubjects');
- describe('in iframe', () => {
+ // Flaky: https://github.com/elastic/kibana/issues/70928
+ describe.skip('in iframe', () => {
it('should open Kibana for logged-in user', async () => {
const isChromeHiddenBefore = await PageObjects.common.isChromeHidden();
expect(isChromeHiddenBefore).to.be(true);
From c5729b87d6c806e5d992f038d219856cdfe08979 Mon Sep 17 00:00:00 2001
From: Michael Hirsch
Date: Mon, 13 Jul 2020 12:35:04 -0400
Subject: [PATCH 21/40] [ML] Adds siem_cloudtrail Module (#71323)
* adds siem_cloudtrail module
* updates logo to logoSecurity
Co-authored-by: Elastic Machine
---
.../modules/siem_cloudtrail/logo.json | 3 +
.../modules/siem_cloudtrail/manifest.json | 64 +++++++++++++++++++
...eed_high_distinct_count_error_message.json | 16 +++++
.../ml/datafeed_rare_error_code.json | 16 +++++
.../ml/datafeed_rare_method_for_a_city.json | 16 +++++
.../datafeed_rare_method_for_a_country.json | 16 +++++
.../datafeed_rare_method_for_a_username.json | 16 +++++
.../ml/high_distinct_count_error_message.json | 33 ++++++++++
.../siem_cloudtrail/ml/rare_error_code.json | 33 ++++++++++
.../ml/rare_method_for_a_city.json | 34 ++++++++++
.../ml/rare_method_for_a_country.json | 34 ++++++++++
.../ml/rare_method_for_a_username.json | 34 ++++++++++
.../apis/ml/modules/get_module.ts | 1 +
13 files changed, 316 insertions(+)
create mode 100644 x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/logo.json
create mode 100644 x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/manifest.json
create mode 100644 x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/datafeed_high_distinct_count_error_message.json
create mode 100644 x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/datafeed_rare_error_code.json
create mode 100644 x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/datafeed_rare_method_for_a_city.json
create mode 100644 x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/datafeed_rare_method_for_a_country.json
create mode 100644 x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/datafeed_rare_method_for_a_username.json
create mode 100644 x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/high_distinct_count_error_message.json
create mode 100644 x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_error_code.json
create mode 100644 x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_method_for_a_city.json
create mode 100644 x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_method_for_a_country.json
create mode 100644 x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_method_for_a_username.json
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/logo.json
new file mode 100644
index 0000000000000..ca61db7992083
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/logo.json
@@ -0,0 +1,3 @@
+{
+ "icon": "logoSecurity"
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/manifest.json
new file mode 100644
index 0000000000000..b7afe8d2b158a
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/manifest.json
@@ -0,0 +1,64 @@
+{
+ "id": "siem_cloudtrail",
+ "title": "SIEM Cloudtrail",
+ "description": "Detect suspicious activity recorded in your cloudtrail logs.",
+ "type": "Filebeat data",
+ "logoFile": "logo.json",
+ "defaultIndexPattern": "filebeat-*",
+ "query": {
+ "bool": {
+ "filter": [
+ {"term": {"event.dataset": "aws.cloudtrail"}}
+ ]
+ }
+ },
+ "jobs": [
+ {
+ "id": "rare_method_for_a_city",
+ "file": "rare_method_for_a_city.json"
+ },
+ {
+ "id": "rare_method_for_a_country",
+ "file": "rare_method_for_a_country.json"
+ },
+ {
+ "id": "rare_method_for_a_username",
+ "file": "rare_method_for_a_username.json"
+ },
+ {
+ "id": "high_distinct_count_error_message",
+ "file": "high_distinct_count_error_message.json"
+ },
+ {
+ "id": "rare_error_code",
+ "file": "rare_error_code.json"
+ }
+ ],
+ "datafeeds": [
+ {
+ "id": "datafeed-rare_method_for_a_city",
+ "file": "datafeed_rare_method_for_a_city.json",
+ "job_id": "rare_method_for_a_city"
+ },
+ {
+ "id": "datafeed-rare_method_for_a_country",
+ "file": "datafeed_rare_method_for_a_country.json",
+ "job_id": "rare_method_for_a_country"
+ },
+ {
+ "id": "datafeed-rare_method_for_a_username",
+ "file": "datafeed_rare_method_for_a_username.json",
+ "job_id": "rare_method_for_a_username"
+ },
+ {
+ "id": "datafeed-high_distinct_count_error_message",
+ "file": "datafeed_high_distinct_count_error_message.json",
+ "job_id": "high_distinct_count_error_message"
+ },
+ {
+ "id": "datafeed-rare_error_code",
+ "file": "datafeed_rare_error_code.json",
+ "job_id": "rare_error_code"
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/datafeed_high_distinct_count_error_message.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/datafeed_high_distinct_count_error_message.json
new file mode 100644
index 0000000000000..269aac2ea72a1
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/datafeed_high_distinct_count_error_message.json
@@ -0,0 +1,16 @@
+{
+ "job_id": "JOB_ID",
+ "indices": [
+ "INDEX_PATTERN_NAME"
+ ],
+ "max_empty_searches": 10,
+ "query": {
+ "bool": {
+ "filter": [
+ {"term": {"event.dataset": "aws.cloudtrail"}},
+ {"term": {"event.module": "aws"}},
+ {"exists": {"field": "aws.cloudtrail.error_message"}}
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/datafeed_rare_error_code.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/datafeed_rare_error_code.json
new file mode 100644
index 0000000000000..4b463a4d10991
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/datafeed_rare_error_code.json
@@ -0,0 +1,16 @@
+{
+ "job_id": "JOB_ID",
+ "indices": [
+ "INDEX_PATTERN_NAME"
+ ],
+ "max_empty_searches": 10,
+ "query": {
+ "bool": {
+ "filter": [
+ {"term": {"event.dataset": "aws.cloudtrail"}},
+ {"term": {"event.module": "aws"}},
+ {"exists": {"field": "aws.cloudtrail.error_code"}}
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/datafeed_rare_method_for_a_city.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/datafeed_rare_method_for_a_city.json
new file mode 100644
index 0000000000000..e436273a848e7
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/datafeed_rare_method_for_a_city.json
@@ -0,0 +1,16 @@
+{
+ "job_id": "JOB_ID",
+ "indices": [
+ "INDEX_PATTERN_NAME"
+ ],
+ "max_empty_searches": 10,
+ "query": {
+ "bool": {
+ "filter": [
+ {"term": {"event.dataset": "aws.cloudtrail"}},
+ {"term": {"event.module": "aws"}},
+ {"exists": {"field": "source.geo.city_name"}}
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/datafeed_rare_method_for_a_country.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/datafeed_rare_method_for_a_country.json
new file mode 100644
index 0000000000000..f0e80174b8791
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/datafeed_rare_method_for_a_country.json
@@ -0,0 +1,16 @@
+{
+ "job_id": "JOB_ID",
+ "indices": [
+ "INDEX_PATTERN_NAME"
+ ],
+ "max_empty_searches": 10,
+ "query": {
+ "bool": {
+ "filter": [
+ {"term": {"event.dataset": "aws.cloudtrail"}},
+ {"term": {"event.module": "aws"}},
+ {"exists": {"field": "source.geo.country_iso_code"}}
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/datafeed_rare_method_for_a_username.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/datafeed_rare_method_for_a_username.json
new file mode 100644
index 0000000000000..2fd3622ff81ce
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/datafeed_rare_method_for_a_username.json
@@ -0,0 +1,16 @@
+{
+ "job_id": "JOB_ID",
+ "indices": [
+ "INDEX_PATTERN_NAME"
+ ],
+ "max_empty_searches": 10,
+ "query": {
+ "bool": {
+ "filter": [
+ {"term": {"event.dataset": "aws.cloudtrail"}},
+ {"term": {"event.module": "aws"}},
+ {"exists": {"field": "user.name"}}
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/high_distinct_count_error_message.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/high_distinct_count_error_message.json
new file mode 100644
index 0000000000000..fdabf66ac91b3
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/high_distinct_count_error_message.json
@@ -0,0 +1,33 @@
+{
+ "job_type": "anomaly_detector",
+ "description": "Looks for a spike in the rate of an error message which may simply indicate an impending service failure but these can also be byproducts of attempted or successful persistence, privilege escalation, defense evasion, discovery, lateral movement, or collection activity by a threat actor.",
+ "groups": [
+ "siem",
+ "cloudtrail"
+ ],
+ "analysis_config": {
+ "bucket_span": "15m",
+ "detectors": [
+ {
+ "detector_description": "high_distinct_count(\"aws.cloudtrail.error_message\")",
+ "function": "high_distinct_count",
+ "field_name": "aws.cloudtrail.error_message"
+ }
+ ],
+ "influencers": [
+ "aws.cloudtrail.user_identity.arn",
+ "source.ip",
+ "source.geo.city_name"
+ ]
+ },
+ "allow_lazy_open": true,
+ "analysis_limits": {
+ "model_memory_limit": "16mb"
+ },
+ "data_description": {
+ "time_field": "@timestamp"
+ },
+ "custom_settings": {
+ "created_by": "ml-module-siem-cloudtrail"
+ }
+ }
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_error_code.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_error_code.json
new file mode 100644
index 0000000000000..0f8fa814ac60a
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_error_code.json
@@ -0,0 +1,33 @@
+{
+ "job_type": "anomaly_detector",
+ "description": "Looks for unsual errors. Rare and unusual errors may simply indicate an impending service failure but they can also be byproducts of attempted or successful persistence, privilege escalation, defense evasion, discovery, lateral movement, or collection activity by a threat actor.",
+ "groups": [
+ "siem",
+ "cloudtrail"
+ ],
+ "analysis_config": {
+ "bucket_span": "60m",
+ "detectors": [
+ {
+ "detector_description": "rare by \"aws.cloudtrail.error_code\"",
+ "function": "rare",
+ "by_field_name": "aws.cloudtrail.error_code"
+ }
+ ],
+ "influencers": [
+ "aws.cloudtrail.user_identity.arn",
+ "source.ip",
+ "source.geo.city_name"
+ ]
+ },
+ "allow_lazy_open": true,
+ "analysis_limits": {
+ "model_memory_limit": "16mb"
+ },
+ "data_description": {
+ "time_field": "@timestamp"
+ },
+ "custom_settings": {
+ "created_by": "ml-module-siem-cloudtrail"
+ }
+ }
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_method_for_a_city.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_method_for_a_city.json
new file mode 100644
index 0000000000000..eff4d4cdbb889
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_method_for_a_city.json
@@ -0,0 +1,34 @@
+{
+ "job_type": "anomaly_detector",
+ "description": "Looks for AWS API calls that, while not inherently suspicious or abnormal, are sourcing from a geolocation (city) that is unusual. This can be the result of compromised credentials or keys.",
+ "groups": [
+ "siem",
+ "cloudtrail"
+ ],
+ "analysis_config": {
+ "bucket_span": "60m",
+ "detectors": [
+ {
+ "detector_description": "rare by \"event.action\" partition by \"source.geo.city_name\"",
+ "function": "rare",
+ "by_field_name": "event.action",
+ "partition_field_name": "source.geo.city_name"
+ }
+ ],
+ "influencers": [
+ "aws.cloudtrail.user_identity.arn",
+ "source.ip",
+ "source.geo.city_name"
+ ]
+ },
+ "allow_lazy_open": true,
+ "analysis_limits": {
+ "model_memory_limit": "64mb"
+ },
+ "data_description": {
+ "time_field": "@timestamp"
+ },
+ "custom_settings": {
+ "created_by": "ml-module-siem-cloudtrail"
+ }
+ }
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_method_for_a_country.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_method_for_a_country.json
new file mode 100644
index 0000000000000..810822c30a5dd
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_method_for_a_country.json
@@ -0,0 +1,34 @@
+{
+ "job_type": "anomaly_detector",
+ "description": "Looks for AWS API calls that, while not inherently suspicious or abnormal, are sourcing from a geolocation (country) that is unusual. This can be the result of compromised credentials or keys.",
+ "groups": [
+ "siem",
+ "cloudtrail"
+ ],
+ "analysis_config": {
+ "bucket_span": "60m",
+ "detectors": [
+ {
+ "detector_description": "rare by \"event.action\" partition by \"source.geo.country_iso_code\"",
+ "function": "rare",
+ "by_field_name": "event.action",
+ "partition_field_name": "source.geo.country_iso_code"
+ }
+ ],
+ "influencers": [
+ "aws.cloudtrail.user_identity.arn",
+ "source.ip",
+ "source.geo.country_iso_code"
+ ]
+ },
+ "allow_lazy_open": true,
+ "analysis_limits": {
+ "model_memory_limit": "64mb"
+ },
+ "data_description": {
+ "time_field": "@timestamp"
+ },
+ "custom_settings": {
+ "created_by": "ml-module-siem-cloudtrail"
+ }
+ }
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_method_for_a_username.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_method_for_a_username.json
new file mode 100644
index 0000000000000..2edf52e8351ed
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/rare_method_for_a_username.json
@@ -0,0 +1,34 @@
+{
+ "job_type": "anomaly_detector",
+ "description": "Looks for AWS API calls that, while not inherently suspicious or abnormal, are sourcing from a user context that does not normally call the method. This can be the result of compromised credentials or keys as someone uses a valid account to persist, move laterally, or exfil data.",
+ "groups": [
+ "siem",
+ "cloudtrail"
+ ],
+ "analysis_config": {
+ "bucket_span": "60m",
+ "detectors": [
+ {
+ "detector_description": "rare by \"event.action\" partition by \"user.name\"",
+ "function": "rare",
+ "by_field_name": "event.action",
+ "partition_field_name": "user.name"
+ }
+ ],
+ "influencers": [
+ "user.name",
+ "source.ip",
+ "source.geo.city_name"
+ ]
+ },
+ "allow_lazy_open": true,
+ "analysis_limits": {
+ "model_memory_limit": "128mb"
+ },
+ "data_description": {
+ "time_field": "@timestamp"
+ },
+ "custom_settings": {
+ "created_by": "ml-module-siem-cloudtrail"
+ }
+ }
\ No newline at end of file
diff --git a/x-pack/test/api_integration/apis/ml/modules/get_module.ts b/x-pack/test/api_integration/apis/ml/modules/get_module.ts
index 5ca496a7a7fe9..cfb3c17ac7f21 100644
--- a/x-pack/test/api_integration/apis/ml/modules/get_module.ts
+++ b/x-pack/test/api_integration/apis/ml/modules/get_module.ts
@@ -25,6 +25,7 @@ const moduleIds = [
'sample_data_weblogs',
'siem_auditbeat',
'siem_auditbeat_auth',
+ 'siem_cloudtrail',
'siem_packetbeat',
'siem_winlogbeat',
'siem_winlogbeat_auth',
From 1a65900e8ed45e17b621234ec64d88ce57ee075e Mon Sep 17 00:00:00 2001
From: Chris Cowan
Date: Mon, 13 Jul 2020 09:51:43 -0700
Subject: [PATCH 22/40] [TSVB] Add support for histogram type (#68837)
* [TSVB] Add support for histogram type
* Merge branch 'master' of github.com:elastic/kibana into issue-52426-tsvb-support-for-histograms
* Adding support to filter ratio; updating test
* Limist aggs for filter_ratio and histogram fields; add test for AggSelect; Fixes #70984
* Ensure only compatible fields are displayed for filter ratio metric aggs
Co-authored-by: Elastic Machine
---
.../common/metric_types.js | 3 +
.../components/aggs/agg_select.test.tsx | 184 ++++++++++++++++++
.../components/aggs/agg_select.tsx | 17 ++
.../components/aggs/filter_ratio.js | 19 +-
.../components/aggs/filter_ratio.test.js | 136 +++++++++++++
.../components/aggs/histogram_support.test.js | 94 +++++++++
.../application/components/aggs/percentile.js | 2 +-
.../aggs/percentile_rank/percentile_rank.tsx | 2 +-
.../components/aggs/positive_rate.js | 2 +-
.../application/components/aggs/std_agg.js | 6 +-
.../get_supported_fields_by_metric_type.js | 34 ++++
...et_supported_fields_by_metric_type.test.js | 44 +++++
.../public/test_utils/index.ts | 50 +++++
13 files changed, 580 insertions(+), 13 deletions(-)
create mode 100644 src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.test.tsx
create mode 100644 src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.test.js
create mode 100644 src/plugins/vis_type_timeseries/public/application/components/aggs/histogram_support.test.js
create mode 100644 src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js
create mode 100644 src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.test.js
create mode 100644 src/plugins/vis_type_timeseries/public/test_utils/index.ts
diff --git a/src/plugins/vis_type_timeseries/common/metric_types.js b/src/plugins/vis_type_timeseries/common/metric_types.js
index 9dc6085b080e9..05836a6df410a 100644
--- a/src/plugins/vis_type_timeseries/common/metric_types.js
+++ b/src/plugins/vis_type_timeseries/common/metric_types.js
@@ -27,6 +27,9 @@ export const METRIC_TYPES = {
VARIANCE: 'variance',
SUM_OF_SQUARES: 'sum_of_squares',
CARDINALITY: 'cardinality',
+ VALUE_COUNT: 'value_count',
+ AVERAGE: 'avg',
+ SUM: 'sum',
};
export const EXTENDED_STATS_TYPES = [
diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.test.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.test.tsx
new file mode 100644
index 0000000000000..968fa5384e1d8
--- /dev/null
+++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.test.tsx
@@ -0,0 +1,184 @@
+/*
+ * 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 React from 'react';
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import { AggSelect } from './agg_select';
+import { METRIC, SERIES } from '../../../test_utils';
+import { EuiComboBox } from '@elastic/eui';
+
+describe('TSVB AggSelect', () => {
+ const setup = (panelType: string, value: string) => {
+ const metric = {
+ ...METRIC,
+ type: 'filter_ratio',
+ field: 'histogram_value',
+ };
+ const series = { ...SERIES, metrics: [metric] };
+
+ const wrapper = mountWithIntl(
+
+ );
+ return wrapper;
+ };
+
+ it('should only display filter ratio compattible aggs', () => {
+ const wrapper = setup('filter_ratio', 'avg');
+ expect(wrapper.find(EuiComboBox).props().options).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "label": "Average",
+ "value": "avg",
+ },
+ Object {
+ "label": "Cardinality",
+ "value": "cardinality",
+ },
+ Object {
+ "label": "Count",
+ "value": "count",
+ },
+ Object {
+ "label": "Positive Rate",
+ "value": "positive_rate",
+ },
+ Object {
+ "label": "Max",
+ "value": "max",
+ },
+ Object {
+ "label": "Min",
+ "value": "min",
+ },
+ Object {
+ "label": "Sum",
+ "value": "sum",
+ },
+ Object {
+ "label": "Value Count",
+ "value": "value_count",
+ },
+ ]
+ `);
+ });
+
+ it('should only display histogram compattible aggs', () => {
+ const wrapper = setup('histogram', 'avg');
+ expect(wrapper.find(EuiComboBox).props().options).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "label": "Average",
+ "value": "avg",
+ },
+ Object {
+ "label": "Count",
+ "value": "count",
+ },
+ Object {
+ "label": "Sum",
+ "value": "sum",
+ },
+ Object {
+ "label": "Value Count",
+ "value": "value_count",
+ },
+ ]
+ `);
+ });
+
+ it('should only display metrics compattible aggs', () => {
+ const wrapper = setup('metrics', 'avg');
+ expect(wrapper.find(EuiComboBox).props().options).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "label": "Average",
+ "value": "avg",
+ },
+ Object {
+ "label": "Cardinality",
+ "value": "cardinality",
+ },
+ Object {
+ "label": "Count",
+ "value": "count",
+ },
+ Object {
+ "label": "Filter Ratio",
+ "value": "filter_ratio",
+ },
+ Object {
+ "label": "Positive Rate",
+ "value": "positive_rate",
+ },
+ Object {
+ "label": "Max",
+ "value": "max",
+ },
+ Object {
+ "label": "Min",
+ "value": "min",
+ },
+ Object {
+ "label": "Percentile",
+ "value": "percentile",
+ },
+ Object {
+ "label": "Percentile Rank",
+ "value": "percentile_rank",
+ },
+ Object {
+ "label": "Static Value",
+ "value": "static",
+ },
+ Object {
+ "label": "Std. Deviation",
+ "value": "std_deviation",
+ },
+ Object {
+ "label": "Sum",
+ "value": "sum",
+ },
+ Object {
+ "label": "Sum of Squares",
+ "value": "sum_of_squares",
+ },
+ Object {
+ "label": "Top Hit",
+ "value": "top_hit",
+ },
+ Object {
+ "label": "Value Count",
+ "value": "value_count",
+ },
+ Object {
+ "label": "Variance",
+ "value": "variance",
+ },
+ ]
+ `);
+ });
+});
diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx
index 6fa1a2adaa08e..7701d351e5478 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx
+++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx
@@ -225,6 +225,19 @@ const specialAggs: AggSelectOption[] = [
},
];
+const FILTER_RATIO_AGGS = [
+ 'avg',
+ 'cardinality',
+ 'count',
+ 'positive_rate',
+ 'max',
+ 'min',
+ 'sum',
+ 'value_count',
+];
+
+const HISTOGRAM_AGGS = ['avg', 'count', 'sum', 'value_count'];
+
const allAggOptions = [...metricAggs, ...pipelineAggs, ...siblingAggs, ...specialAggs];
function filterByPanelType(panelType: string) {
@@ -257,6 +270,10 @@ export function AggSelect(props: AggSelectUiProps) {
let options: EuiComboBoxOptionOption[];
if (panelType === 'metrics') {
options = metricAggs;
+ } else if (panelType === 'filter_ratio') {
+ options = metricAggs.filter((m) => FILTER_RATIO_AGGS.includes(`${m.value}`));
+ } else if (panelType === 'histogram') {
+ options = metricAggs.filter((m) => HISTOGRAM_AGGS.includes(`${m.value}`));
} else {
const disableSiblingAggs = (agg: AggSelectOption) => ({
...agg,
diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js
index b5311e3832da4..2aa994c09a2ad 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js
@@ -36,7 +36,15 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public';
-import { METRIC_TYPES } from '../../../../../../plugins/vis_type_timeseries/common/metric_types';
+import { getSupportedFieldsByMetricType } from '../lib/get_supported_fields_by_metric_type';
+
+const isFieldHistogram = (fields, indexPattern, field) => {
+ const indexFields = fields[indexPattern];
+ if (!indexFields) return false;
+ const fieldObject = indexFields.find((f) => f.name === field);
+ if (!fieldObject) return false;
+ return fieldObject.type === KBN_FIELD_TYPES.HISTOGRAM;
+};
export const FilterRatioAgg = (props) => {
const { series, fields, panel } = props;
@@ -56,9 +64,6 @@ export const FilterRatioAgg = (props) => {
const model = { ...defaults, ...props.model };
const htmlId = htmlIdGenerator();
- const restrictFields =
- model.metric_agg === METRIC_TYPES.CARDINALITY ? [] : [KBN_FIELD_TYPES.NUMBER];
-
return (
{
@@ -149,7 +156,7 @@ export const FilterRatioAgg = (props) => {
{
+ const setup = (metric) => {
+ const series = { ...SERIES, metrics: [metric] };
+ const panel = { ...PANEL, series };
+
+ const wrapper = mountWithIntl(
+
+
+
+ );
+ return wrapper;
+ };
+
+ describe('histogram support', () => {
+ it('should only display histogram compattible aggs', () => {
+ const metric = {
+ ...METRIC,
+ metric_agg: 'avg',
+ field: 'histogram_value',
+ };
+ const wrapper = setup(metric);
+ expect(wrapper.find(EuiComboBox).at(1).props().options).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "label": "Average",
+ "value": "avg",
+ },
+ Object {
+ "label": "Count",
+ "value": "count",
+ },
+ Object {
+ "label": "Sum",
+ "value": "sum",
+ },
+ Object {
+ "label": "Value Count",
+ "value": "value_count",
+ },
+ ]
+ `);
+ });
+ const shouldNotHaveHistogramField = (agg) => {
+ it(`should not have histogram fields for ${agg}`, () => {
+ const metric = {
+ ...METRIC,
+ metric_agg: agg,
+ field: '',
+ };
+ const wrapper = setup(metric);
+ expect(wrapper.find(EuiComboBox).at(2).props().options).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "label": "number",
+ "options": Array [
+ Object {
+ "label": "system.cpu.user.pct",
+ "value": "system.cpu.user.pct",
+ },
+ ],
+ },
+ ]
+ `);
+ });
+ };
+ shouldNotHaveHistogramField('max');
+ shouldNotHaveHistogramField('min');
+ shouldNotHaveHistogramField('positive_rate');
+
+ it(`should not have histogram fields for cardinality`, () => {
+ const metric = {
+ ...METRIC,
+ metric_agg: 'cardinality',
+ field: '',
+ };
+ const wrapper = setup(metric);
+ expect(wrapper.find(EuiComboBox).at(2).props().options).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "label": "date",
+ "options": Array [
+ Object {
+ "label": "@timestamp",
+ "value": "@timestamp",
+ },
+ ],
+ },
+ Object {
+ "label": "number",
+ "options": Array [
+ Object {
+ "label": "system.cpu.user.pct",
+ "value": "system.cpu.user.pct",
+ },
+ ],
+ },
+ ]
+ `);
+ });
+ });
+});
diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/histogram_support.test.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/histogram_support.test.js
new file mode 100644
index 0000000000000..7af33ba11f247
--- /dev/null
+++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/histogram_support.test.js
@@ -0,0 +1,94 @@
+/*
+ * 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 React from 'react';
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import { Agg } from './agg';
+import { FieldSelect } from './field_select';
+import { FIELDS, METRIC, SERIES, PANEL } from '../../../test_utils';
+const runTest = (aggType, name, test, additionalProps = {}) => {
+ describe(aggType, () => {
+ const metric = {
+ ...METRIC,
+ type: aggType,
+ field: 'histogram_value',
+ ...additionalProps,
+ };
+ const series = { ...SERIES, metrics: [metric] };
+ const panel = { ...PANEL, series };
+
+ it(name, () => {
+ const wrapper = mountWithIntl(
+
+ );
+ test(wrapper);
+ });
+ });
+};
+
+describe('Histogram Types', () => {
+ describe('supported', () => {
+ const shouldHaveHistogramSupport = (aggType, additionalProps = {}) => {
+ runTest(
+ aggType,
+ 'supports',
+ (wrapper) =>
+ expect(wrapper.find(FieldSelect).at(0).props().restrict).toContain('histogram'),
+ additionalProps
+ );
+ };
+ shouldHaveHistogramSupport('avg');
+ shouldHaveHistogramSupport('sum');
+ shouldHaveHistogramSupport('value_count');
+ shouldHaveHistogramSupport('percentile');
+ shouldHaveHistogramSupport('percentile_rank');
+ shouldHaveHistogramSupport('filter_ratio', { metric_agg: 'avg' });
+ });
+ describe('not supported', () => {
+ const shouldNotHaveHistogramSupport = (aggType, additionalProps = {}) => {
+ runTest(
+ aggType,
+ 'does not support',
+ (wrapper) =>
+ expect(wrapper.find(FieldSelect).at(0).props().restrict).not.toContain('histogram'),
+ additionalProps
+ );
+ };
+ shouldNotHaveHistogramSupport('cardinality');
+ shouldNotHaveHistogramSupport('max');
+ shouldNotHaveHistogramSupport('min');
+ shouldNotHaveHistogramSupport('variance');
+ shouldNotHaveHistogramSupport('sum_of_squares');
+ shouldNotHaveHistogramSupport('std_deviation');
+ shouldNotHaveHistogramSupport('positive_rate');
+ shouldNotHaveHistogramSupport('top_hit');
+ });
+});
diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js
index 6a7bf1bffe83c..f12c0c8f6f465 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js
@@ -36,7 +36,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public';
import { Percentiles, newPercentile } from './percentile_ui';
-const RESTRICT_FIELDS = [KBN_FIELD_TYPES.NUMBER];
+const RESTRICT_FIELDS = [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.HISTOGRAM];
const checkModel = (model) => Array.isArray(model.percentiles);
diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx
index a16f5aeefc49c..d02a16ade2bba 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx
+++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx
@@ -41,7 +41,7 @@ import { IFieldType, KBN_FIELD_TYPES } from '../../../../../../../plugins/data/p
import { MetricsItemsSchema, PanelSchema, SeriesItemsSchema } from '../../../../../common/types';
import { DragHandleProps } from '../../../../types';
-const RESTRICT_FIELDS = [KBN_FIELD_TYPES.NUMBER];
+const RESTRICT_FIELDS = [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.HISTOGRAM];
interface PercentileRankAggProps {
disableDelete: boolean;
diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js
index 3ca89f7289d65..c20bcc1babc1d 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js
@@ -123,7 +123,7 @@ export const PositiveRateAgg = (props) => {
t !== KBN_FIELD_TYPES.HISTOGRAM);
+ case METRIC_TYPES.VALUE_COUNT:
+ case METRIC_TYPES.AVERAGE:
+ case METRIC_TYPES.SUM:
+ return [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.HISTOGRAM];
+ default:
+ return [KBN_FIELD_TYPES.NUMBER];
+ }
+}
diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.test.js
new file mode 100644
index 0000000000000..3cd3fac191bf1
--- /dev/null
+++ b/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.test.js
@@ -0,0 +1,44 @@
+/*
+ * 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 { getSupportedFieldsByMetricType } from './get_supported_fields_by_metric_type';
+
+describe('getSupportedFieldsByMetricType', () => {
+ const shouldHaveHistogramAndNumbers = (type) =>
+ it(`should return numbers and histogram for ${type}`, () => {
+ expect(getSupportedFieldsByMetricType(type)).toEqual(['number', 'histogram']);
+ });
+ const shouldHaveOnlyNumbers = (type) =>
+ it(`should return only numbers for ${type}`, () => {
+ expect(getSupportedFieldsByMetricType(type)).toEqual(['number']);
+ });
+
+ shouldHaveHistogramAndNumbers('value_count');
+ shouldHaveHistogramAndNumbers('avg');
+ shouldHaveHistogramAndNumbers('sum');
+
+ shouldHaveOnlyNumbers('positive_rate');
+ shouldHaveOnlyNumbers('std_deviation');
+ shouldHaveOnlyNumbers('max');
+ shouldHaveOnlyNumbers('min');
+
+ it(`should return everything but histogram for cardinality`, () => {
+ expect(getSupportedFieldsByMetricType('cardinality')).not.toContain('histogram');
+ });
+});
diff --git a/src/plugins/vis_type_timeseries/public/test_utils/index.ts b/src/plugins/vis_type_timeseries/public/test_utils/index.ts
new file mode 100644
index 0000000000000..96ecc89b70c2d
--- /dev/null
+++ b/src/plugins/vis_type_timeseries/public/test_utils/index.ts
@@ -0,0 +1,50 @@
+/*
+ * 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 const UI_RESTRICTIONS = { '*': true };
+export const INDEX_PATTERN = 'some-pattern';
+export const FIELDS = {
+ [INDEX_PATTERN]: [
+ {
+ type: 'date',
+ name: '@timestamp',
+ },
+ {
+ type: 'number',
+ name: 'system.cpu.user.pct',
+ },
+ {
+ type: 'histogram',
+ name: 'histogram_value',
+ },
+ ],
+};
+export const METRIC = {
+ id: 'sample_metric',
+ type: 'avg',
+ field: 'system.cpu.user.pct',
+};
+export const SERIES = {
+ metrics: [METRIC],
+};
+export const PANEL = {
+ type: 'timeseries',
+ index_pattern: INDEX_PATTERN,
+ series: SERIES,
+};
From 4db58164597638e20f255f6450a9a29c97618a07 Mon Sep 17 00:00:00 2001
From: Kerry Gallagher
Date: Mon, 13 Jul 2020 18:09:10 +0100
Subject: [PATCH 23/40] [Logs UI] Add category anomalies to anomalies page
(#70982)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Add category anomalies to anomalies page
Co-authored-by: Felix Stürmer
Co-authored-by: Elastic Machine
---
.../http_api/log_analysis/results/index.ts | 3 +-
.../results/log_entry_anomalies.ts | 137 ++++++
.../results/log_entry_examples.ts | 82 ++++
.../results/log_entry_rate_examples.ts | 77 ----
.../log_analysis/log_analysis_results.ts | 4 +
.../log_entry_rate/page_results_content.tsx | 121 +++---
.../sections/anomalies/chart.tsx | 97 +++--
.../sections/anomalies/expanded_row.tsx | 58 ++-
.../sections/anomalies/index.tsx | 162 ++++---
.../sections/anomalies/log_entry_example.tsx | 22 +-
.../sections/anomalies/table.tsx | 303 +++++++------
.../sections/log_rate/bar_chart.tsx | 100 -----
.../sections/log_rate/index.tsx | 98 -----
.../service_calls/get_log_entry_anomalies.ts | 41 ++
..._examples.ts => get_log_entry_examples.ts} | 14 +-
.../use_log_entry_anomalies_results.ts | 262 ++++++++++++
.../log_entry_rate/use_log_entry_examples.ts | 65 +++
.../use_log_entry_rate_examples.ts | 63 ---
x-pack/plugins/infra/server/infra_server.ts | 6 +-
.../infra/server/lib/log_analysis/common.ts | 29 ++
.../infra/server/lib/log_analysis/errors.ts | 7 +
.../infra/server/lib/log_analysis/index.ts | 1 +
.../lib/log_analysis/log_entry_anomalies.ts | 398 ++++++++++++++++++
.../log_entry_categories_analysis.ts | 30 +-
.../log_analysis/log_entry_rate_analysis.ts | 145 +------
.../server/lib/log_analysis/queries/common.ts | 8 +
.../server/lib/log_analysis/queries/index.ts | 1 +
.../queries/log_entry_anomalies.ts | 128 ++++++
...rate_examples.ts => log_entry_examples.ts} | 41 +-
.../routes/log_analysis/results/index.ts | 3 +-
.../results/log_entry_anomalies.ts | 112 +++++
...rate_examples.ts => log_entry_examples.ts} | 20 +-
.../translations/translations/ja-JP.json | 9 -
.../translations/translations/zh-CN.json | 9 -
34 files changed, 1764 insertions(+), 892 deletions(-)
create mode 100644 x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_anomalies.ts
create mode 100644 x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_examples.ts
delete mode 100644 x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_rate_examples.ts
delete mode 100644 x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/log_rate/bar_chart.tsx
delete mode 100644 x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/log_rate/index.tsx
create mode 100644 x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_anomalies.ts
rename x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/{get_log_entry_rate_examples.ts => get_log_entry_examples.ts} (77%)
create mode 100644 x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_anomalies_results.ts
create mode 100644 x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_examples.ts
delete mode 100644 x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_examples.ts
create mode 100644 x-pack/plugins/infra/server/lib/log_analysis/common.ts
create mode 100644 x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts
create mode 100644 x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_anomalies.ts
rename x-pack/plugins/infra/server/lib/log_analysis/queries/{log_entry_rate_examples.ts => log_entry_examples.ts} (59%)
create mode 100644 x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_anomalies.ts
rename x-pack/plugins/infra/server/routes/log_analysis/results/{log_entry_rate_examples.ts => log_entry_examples.ts} (75%)
diff --git a/x-pack/plugins/infra/common/http_api/log_analysis/results/index.ts b/x-pack/plugins/infra/common/http_api/log_analysis/results/index.ts
index 30b6be435837b..cbd89db97236f 100644
--- a/x-pack/plugins/infra/common/http_api/log_analysis/results/index.ts
+++ b/x-pack/plugins/infra/common/http_api/log_analysis/results/index.ts
@@ -8,4 +8,5 @@ export * from './log_entry_categories';
export * from './log_entry_category_datasets';
export * from './log_entry_category_examples';
export * from './log_entry_rate';
-export * from './log_entry_rate_examples';
+export * from './log_entry_examples';
+export * from './log_entry_anomalies';
diff --git a/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_anomalies.ts b/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_anomalies.ts
new file mode 100644
index 0000000000000..639ac63f9b14d
--- /dev/null
+++ b/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_anomalies.ts
@@ -0,0 +1,137 @@
+/*
+ * 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 * as rt from 'io-ts';
+
+import { timeRangeRT, routeTimingMetadataRT } from '../../shared';
+
+export const LOG_ANALYSIS_GET_LOG_ENTRY_ANOMALIES_PATH =
+ '/api/infra/log_analysis/results/log_entry_anomalies';
+
+// [Sort field value, tiebreaker value]
+const paginationCursorRT = rt.tuple([
+ rt.union([rt.string, rt.number]),
+ rt.union([rt.string, rt.number]),
+]);
+
+export type PaginationCursor = rt.TypeOf;
+
+export const anomalyTypeRT = rt.keyof({
+ logRate: null,
+ logCategory: null,
+});
+
+export type AnomalyType = rt.TypeOf;
+
+const logEntryAnomalyCommonFieldsRT = rt.type({
+ id: rt.string,
+ anomalyScore: rt.number,
+ dataset: rt.string,
+ typical: rt.number,
+ actual: rt.number,
+ type: anomalyTypeRT,
+ duration: rt.number,
+ startTime: rt.number,
+ jobId: rt.string,
+});
+const logEntrylogRateAnomalyRT = logEntryAnomalyCommonFieldsRT;
+const logEntrylogCategoryAnomalyRT = rt.partial({
+ categoryId: rt.string,
+});
+const logEntryAnomalyRT = rt.intersection([
+ logEntryAnomalyCommonFieldsRT,
+ logEntrylogRateAnomalyRT,
+ logEntrylogCategoryAnomalyRT,
+]);
+
+export type LogEntryAnomaly = rt.TypeOf;
+
+export const getLogEntryAnomaliesSuccessReponsePayloadRT = rt.intersection([
+ rt.type({
+ data: rt.intersection([
+ rt.type({
+ anomalies: rt.array(logEntryAnomalyRT),
+ // Signifies there are more entries backwards or forwards. If this was a request
+ // for a previous page, there are more previous pages, if this was a request for a next page,
+ // there are more next pages.
+ hasMoreEntries: rt.boolean,
+ }),
+ rt.partial({
+ paginationCursors: rt.type({
+ // The cursor to use to fetch the previous page
+ previousPageCursor: paginationCursorRT,
+ // The cursor to use to fetch the next page
+ nextPageCursor: paginationCursorRT,
+ }),
+ }),
+ ]),
+ }),
+ rt.partial({
+ timing: routeTimingMetadataRT,
+ }),
+]);
+
+export type GetLogEntryAnomaliesSuccessResponsePayload = rt.TypeOf<
+ typeof getLogEntryAnomaliesSuccessReponsePayloadRT
+>;
+
+const sortOptionsRT = rt.keyof({
+ anomalyScore: null,
+ dataset: null,
+ startTime: null,
+});
+
+const sortDirectionsRT = rt.keyof({
+ asc: null,
+ desc: null,
+});
+
+const paginationPreviousPageCursorRT = rt.type({
+ searchBefore: paginationCursorRT,
+});
+
+const paginationNextPageCursorRT = rt.type({
+ searchAfter: paginationCursorRT,
+});
+
+const paginationRT = rt.intersection([
+ rt.type({
+ pageSize: rt.number,
+ }),
+ rt.partial({
+ cursor: rt.union([paginationPreviousPageCursorRT, paginationNextPageCursorRT]),
+ }),
+]);
+
+export type Pagination = rt.TypeOf;
+
+const sortRT = rt.type({
+ field: sortOptionsRT,
+ direction: sortDirectionsRT,
+});
+
+export type Sort = rt.TypeOf;
+
+export const getLogEntryAnomaliesRequestPayloadRT = rt.type({
+ data: rt.intersection([
+ rt.type({
+ // the ID of the source configuration
+ sourceId: rt.string,
+ // the time range to fetch the log entry anomalies from
+ timeRange: timeRangeRT,
+ }),
+ rt.partial({
+ // Pagination properties
+ pagination: paginationRT,
+ // Sort properties
+ sort: sortRT,
+ }),
+ ]),
+});
+
+export type GetLogEntryAnomaliesRequestPayload = rt.TypeOf<
+ typeof getLogEntryAnomaliesRequestPayloadRT
+>;
diff --git a/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_examples.ts b/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_examples.ts
new file mode 100644
index 0000000000000..1eed29cd37560
--- /dev/null
+++ b/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_examples.ts
@@ -0,0 +1,82 @@
+/*
+ * 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 * as rt from 'io-ts';
+
+import {
+ badRequestErrorRT,
+ forbiddenErrorRT,
+ timeRangeRT,
+ routeTimingMetadataRT,
+} from '../../shared';
+
+export const LOG_ANALYSIS_GET_LOG_ENTRY_RATE_EXAMPLES_PATH =
+ '/api/infra/log_analysis/results/log_entry_examples';
+
+/**
+ * request
+ */
+
+export const getLogEntryExamplesRequestPayloadRT = rt.type({
+ data: rt.intersection([
+ rt.type({
+ // the dataset to fetch the log rate examples from
+ dataset: rt.string,
+ // the number of examples to fetch
+ exampleCount: rt.number,
+ // the id of the source configuration
+ sourceId: rt.string,
+ // the time range to fetch the log rate examples from
+ timeRange: timeRangeRT,
+ }),
+ rt.partial({
+ categoryId: rt.string,
+ }),
+ ]),
+});
+
+export type GetLogEntryExamplesRequestPayload = rt.TypeOf<
+ typeof getLogEntryExamplesRequestPayloadRT
+>;
+
+/**
+ * response
+ */
+
+const logEntryExampleRT = rt.type({
+ id: rt.string,
+ dataset: rt.string,
+ message: rt.string,
+ timestamp: rt.number,
+ tiebreaker: rt.number,
+});
+
+export type LogEntryExample = rt.TypeOf;
+
+export const getLogEntryExamplesSuccessReponsePayloadRT = rt.intersection([
+ rt.type({
+ data: rt.type({
+ examples: rt.array(logEntryExampleRT),
+ }),
+ }),
+ rt.partial({
+ timing: routeTimingMetadataRT,
+ }),
+]);
+
+export type GetLogEntryExamplesSuccessReponsePayload = rt.TypeOf<
+ typeof getLogEntryExamplesSuccessReponsePayloadRT
+>;
+
+export const getLogEntryExamplesResponsePayloadRT = rt.union([
+ getLogEntryExamplesSuccessReponsePayloadRT,
+ badRequestErrorRT,
+ forbiddenErrorRT,
+]);
+
+export type GetLogEntryExamplesResponsePayload = rt.TypeOf<
+ typeof getLogEntryExamplesResponsePayloadRT
+>;
diff --git a/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_rate_examples.ts b/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_rate_examples.ts
deleted file mode 100644
index 700f87ec3beb1..0000000000000
--- a/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_rate_examples.ts
+++ /dev/null
@@ -1,77 +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 * as rt from 'io-ts';
-
-import {
- badRequestErrorRT,
- forbiddenErrorRT,
- timeRangeRT,
- routeTimingMetadataRT,
-} from '../../shared';
-
-export const LOG_ANALYSIS_GET_LOG_ENTRY_RATE_EXAMPLES_PATH =
- '/api/infra/log_analysis/results/log_entry_rate_examples';
-
-/**
- * request
- */
-
-export const getLogEntryRateExamplesRequestPayloadRT = rt.type({
- data: rt.type({
- // the dataset to fetch the log rate examples from
- dataset: rt.string,
- // the number of examples to fetch
- exampleCount: rt.number,
- // the id of the source configuration
- sourceId: rt.string,
- // the time range to fetch the log rate examples from
- timeRange: timeRangeRT,
- }),
-});
-
-export type GetLogEntryRateExamplesRequestPayload = rt.TypeOf<
- typeof getLogEntryRateExamplesRequestPayloadRT
->;
-
-/**
- * response
- */
-
-const logEntryRateExampleRT = rt.type({
- id: rt.string,
- dataset: rt.string,
- message: rt.string,
- timestamp: rt.number,
- tiebreaker: rt.number,
-});
-
-export type LogEntryRateExample = rt.TypeOf;
-
-export const getLogEntryRateExamplesSuccessReponsePayloadRT = rt.intersection([
- rt.type({
- data: rt.type({
- examples: rt.array(logEntryRateExampleRT),
- }),
- }),
- rt.partial({
- timing: routeTimingMetadataRT,
- }),
-]);
-
-export type GetLogEntryRateExamplesSuccessReponsePayload = rt.TypeOf<
- typeof getLogEntryRateExamplesSuccessReponsePayloadRT
->;
-
-export const getLogEntryRateExamplesResponsePayloadRT = rt.union([
- getLogEntryRateExamplesSuccessReponsePayloadRT,
- badRequestErrorRT,
- forbiddenErrorRT,
-]);
-
-export type GetLogEntryRateExamplesResponsePayload = rt.TypeOf<
- typeof getLogEntryRateExamplesResponsePayloadRT
->;
diff --git a/x-pack/plugins/infra/common/log_analysis/log_analysis_results.ts b/x-pack/plugins/infra/common/log_analysis/log_analysis_results.ts
index 19c92cb381104..f4497dbba5056 100644
--- a/x-pack/plugins/infra/common/log_analysis/log_analysis_results.ts
+++ b/x-pack/plugins/infra/common/log_analysis/log_analysis_results.ts
@@ -41,6 +41,10 @@ export const formatAnomalyScore = (score: number) => {
return Math.round(score);
};
+export const formatOneDecimalPlace = (number: number) => {
+ return Math.round(number * 10) / 10;
+};
+
export const getFriendlyNameForPartitionId = (partitionId: string) => {
return partitionId !== '' ? partitionId : 'unknown';
};
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx
index bf4dbcd87cc41..21c3e3ec70029 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx
@@ -5,30 +5,18 @@
*/
import datemath from '@elastic/datemath';
-import {
- EuiBadge,
- EuiFlexGroup,
- EuiFlexItem,
- EuiPage,
- EuiPanel,
- EuiSuperDatePicker,
- EuiText,
-} from '@elastic/eui';
-import numeral from '@elastic/numeral';
-import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiPanel, EuiSuperDatePicker } from '@elastic/eui';
import moment from 'moment';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { euiStyled, useTrackPageview } from '../../../../../observability/public';
import { TimeRange } from '../../../../common/http_api/shared/time_range';
import { bucketSpan } from '../../../../common/log_analysis';
-import { LoadingOverlayWrapper } from '../../../components/loading_overlay_wrapper';
import { LogAnalysisJobProblemIndicator } from '../../../components/logging/log_analysis_job_status';
import { useInterval } from '../../../hooks/use_interval';
-import { useKibanaUiSetting } from '../../../utils/use_kibana_ui_setting';
import { AnomaliesResults } from './sections/anomalies';
-import { LogRateResults } from './sections/log_rate';
import { useLogEntryRateModuleContext } from './use_log_entry_rate_module';
import { useLogEntryRateResults } from './use_log_entry_rate_results';
+import { useLogEntryAnomaliesResults } from './use_log_entry_anomalies_results';
import {
StringTimeRange,
useLogAnalysisResultsUrlState,
@@ -36,6 +24,15 @@ import {
const JOB_STATUS_POLLING_INTERVAL = 30000;
+export const SORT_DEFAULTS = {
+ direction: 'desc' as const,
+ field: 'anomalyScore' as const,
+};
+
+export const PAGINATION_DEFAULTS = {
+ pageSize: 25,
+};
+
interface LogEntryRateResultsContentProps {
onOpenSetup: () => void;
}
@@ -46,8 +43,6 @@ export const LogEntryRateResultsContent: React.FunctionComponent {
setQueryTimeRange({
@@ -182,45 +194,18 @@ export const LogEntryRateResultsContent: React.FunctionComponent
-
-
-
- {logEntryRate ? (
-
-
-
-
- {numeral(logEntryRate.totalNumberOfLogEntries).format('0.00a')}
-
-
- ),
- startTime: (
- {moment(queryTimeRange.value.startTime).format(dateFormat)}
- ),
- endTime: {moment(queryTimeRange.value.endTime).format(dateFormat)},
- }}
- />
-
-
- ) : null}
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/chart.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/chart.tsx
index 79ab4475ee5a3..ae5c3b5b93b47 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/chart.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/chart.tsx
@@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-
+import { EuiEmptyPrompt } from '@elastic/eui';
import { RectAnnotationDatum, AnnotationId } from '@elastic/charts';
import {
Axis,
@@ -21,6 +21,7 @@ import numeral from '@elastic/numeral';
import { i18n } from '@kbn/i18n';
import moment from 'moment';
import React, { useCallback, useMemo } from 'react';
+import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay_wrapper';
import { TimeRange } from '../../../../../../common/http_api/shared/time_range';
import {
@@ -36,7 +37,16 @@ export const AnomaliesChart: React.FunctionComponent<{
series: Array<{ time: number; value: number }>;
annotations: Record;
renderAnnotationTooltip?: (details?: string) => JSX.Element;
-}> = ({ chartId, series, annotations, setTimeRange, timeRange, renderAnnotationTooltip }) => {
+ isLoading: boolean;
+}> = ({
+ chartId,
+ series,
+ annotations,
+ setTimeRange,
+ timeRange,
+ renderAnnotationTooltip,
+ isLoading,
+}) => {
const [dateFormat] = useKibanaUiSetting('dateFormat', 'Y-MM-DD HH:mm:ss.SSS');
const [isDarkMode] = useKibanaUiSetting('theme:darkMode');
@@ -68,41 +78,56 @@ export const AnomaliesChart: React.FunctionComponent<{
[setTimeRange]
);
- return (
-
-
-
- numeral(value.toPrecision(3)).format('0[.][00]a')} // https://github.com/adamwdraper/Numeral-js/issues/194
- />
-
+ {i18n.translate('xpack.infra.logs.analysis.anomalySectionLogRateChartNoData', {
+ defaultMessage: 'There is no log rate data to display.',
})}
- xScaleType="time"
- yScaleType="linear"
- xAccessor={'time'}
- yAccessors={['value']}
- data={series}
- barSeriesStyle={barSeriesStyle}
- />
- {renderAnnotations(annotations, chartId, renderAnnotationTooltip)}
-
-
-
+
+ }
+ titleSize="m"
+ />
+ ) : (
+
+
+ {series.length ? (
+
+
+ numeral(value.toPrecision(3)).format('0[.][00]a')} // https://github.com/adamwdraper/Numeral-js/issues/194
+ />
+
+ {renderAnnotations(annotations, chartId, renderAnnotationTooltip)}
+
+
+ ) : null}
+
+
);
};
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx
index c527b8c49d099..e4b12e199a048 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx
@@ -10,12 +10,12 @@ import { i18n } from '@kbn/i18n';
import React from 'react';
import { useMount } from 'react-use';
import { TimeRange } from '../../../../../../common/http_api/shared/time_range';
-import { AnomalyRecord } from '../../use_log_entry_rate_results';
-import { useLogEntryRateModuleContext } from '../../use_log_entry_rate_module';
-import { useLogEntryRateExamples } from '../../use_log_entry_rate_examples';
+import { LogEntryAnomaly } from '../../../../../../common/http_api';
+import { useLogEntryExamples } from '../../use_log_entry_examples';
import { LogEntryExampleMessages } from '../../../../../components/logging/log_entry_examples/log_entry_examples';
-import { LogEntryRateExampleMessage, LogEntryRateExampleMessageHeaders } from './log_entry_example';
+import { LogEntryExampleMessage, LogEntryExampleMessageHeaders } from './log_entry_example';
import { euiStyled } from '../../../../../../../observability/public';
+import { useLogSourceContext } from '../../../../../containers/logs/log_source';
const EXAMPLE_COUNT = 5;
@@ -24,29 +24,27 @@ const examplesTitle = i18n.translate('xpack.infra.logs.analysis.anomaliesTableEx
});
export const AnomaliesTableExpandedRow: React.FunctionComponent<{
- anomaly: AnomalyRecord;
+ anomaly: LogEntryAnomaly;
timeRange: TimeRange;
- jobId: string;
-}> = ({ anomaly, timeRange, jobId }) => {
- const {
- sourceConfiguration: { sourceId },
- } = useLogEntryRateModuleContext();
+}> = ({ anomaly, timeRange }) => {
+ const { sourceId } = useLogSourceContext();
const {
- getLogEntryRateExamples,
- hasFailedLoadingLogEntryRateExamples,
- isLoadingLogEntryRateExamples,
- logEntryRateExamples,
- } = useLogEntryRateExamples({
- dataset: anomaly.partitionId,
+ getLogEntryExamples,
+ hasFailedLoadingLogEntryExamples,
+ isLoadingLogEntryExamples,
+ logEntryExamples,
+ } = useLogEntryExamples({
+ dataset: anomaly.dataset,
endTime: anomaly.startTime + anomaly.duration,
exampleCount: EXAMPLE_COUNT,
sourceId,
startTime: anomaly.startTime,
+ categoryId: anomaly.categoryId,
});
useMount(() => {
- getLogEntryRateExamples();
+ getLogEntryExamples();
});
return (
@@ -57,17 +55,17 @@ export const AnomaliesTableExpandedRow: React.FunctionComponent<{
{examplesTitle}
0}
+ isLoading={isLoadingLogEntryExamples}
+ hasFailedLoading={hasFailedLoadingLogEntryExamples}
+ hasResults={logEntryExamples.length > 0}
exampleCount={EXAMPLE_COUNT}
- onReload={getLogEntryRateExamples}
+ onReload={getLogEntryExamples}
>
- {logEntryRateExamples.length > 0 ? (
+ {logEntryExamples.length > 0 ? (
<>
-
- {logEntryRateExamples.map((example, exampleIndex) => (
-
+ {logEntryExamples.map((example, exampleIndex) => (
+
))}
>
@@ -87,11 +85,11 @@ export const AnomaliesTableExpandedRow: React.FunctionComponent<{
void;
timeRange: TimeRange;
viewSetupForReconfiguration: () => void;
- jobId: string;
-}> = ({ isLoading, results, setTimeRange, timeRange, viewSetupForReconfiguration, jobId }) => {
- const hasAnomalies = useMemo(() => {
- return results && results.histogramBuckets
- ? results.histogramBuckets.some((bucket) => {
- return bucket.partitions.some((partition) => {
- return partition.anomalies.length > 0;
- });
- })
- : false;
- }, [results]);
-
+ page: Page;
+ fetchNextPage?: FetchNextPage;
+ fetchPreviousPage?: FetchPreviousPage;
+ changeSortOptions: ChangeSortOptions;
+ changePaginationOptions: ChangePaginationOptions;
+ sortOptions: SortOptions;
+ paginationOptions: PaginationOptions;
+}> = ({
+ isLoadingLogRateResults,
+ isLoadingAnomaliesResults,
+ logEntryRateResults,
+ setTimeRange,
+ timeRange,
+ viewSetupForReconfiguration,
+ anomalies,
+ changeSortOptions,
+ sortOptions,
+ changePaginationOptions,
+ paginationOptions,
+ fetchNextPage,
+ fetchPreviousPage,
+ page,
+}) => {
const logEntryRateSeries = useMemo(
- () => (results && results.histogramBuckets ? getLogEntryRateCombinedSeries(results) : []),
- [results]
+ () =>
+ logEntryRateResults && logEntryRateResults.histogramBuckets
+ ? getLogEntryRateCombinedSeries(logEntryRateResults)
+ : [],
+ [logEntryRateResults]
);
const anomalyAnnotations = useMemo(
() =>
- results && results.histogramBuckets
- ? getAnnotationsForAll(results)
+ logEntryRateResults && logEntryRateResults.histogramBuckets
+ ? getAnnotationsForAll(logEntryRateResults)
: {
warning: [],
minor: [],
major: [],
critical: [],
},
- [results]
+ [logEntryRateResults]
);
return (
<>
-
- {title}
+
+ {title}
-
-
-
- }>
- {!results || (results && results.histogramBuckets && !results.histogramBuckets.length) ? (
+ {(!logEntryRateResults ||
+ (logEntryRateResults &&
+ logEntryRateResults.histogramBuckets &&
+ !logEntryRateResults.histogramBuckets.length)) &&
+ (!anomalies || anomalies.length === 0) ? (
+ }
+ >
@@ -94,41 +123,38 @@ export const AnomaliesResults: React.FunctionComponent<{
}
/>
- ) : !hasAnomalies ? (
-
- {i18n.translate('xpack.infra.logs.analysis.anomalySectionNoAnomaliesTitle', {
- defaultMessage: 'No anomalies were detected.',
- })}
-
- }
- titleSize="m"
+
+ ) : (
+ <>
+
+
+
+
+
+
+
- ) : (
- <>
-
-
-
-
-
-
-
- >
- )}
-
+ >
+ )}
>
);
};
@@ -137,13 +163,6 @@ const title = i18n.translate('xpack.infra.logs.analysis.anomaliesSectionTitle',
defaultMessage: 'Anomalies',
});
-const loadingAriaLabel = i18n.translate(
- 'xpack.infra.logs.analysis.anomaliesSectionLoadingAriaLabel',
- { defaultMessage: 'Loading anomalies' }
-);
-
-const LoadingOverlayContent = () => ;
-
interface ParsedAnnotationDetails {
anomalyScoresByPartition: Array<{ partitionName: string; maximumAnomalyScore: number }>;
}
@@ -189,3 +208,10 @@ const renderAnnotationTooltip = (details?: string) => {
const TooltipWrapper = euiStyled('div')`
white-space: nowrap;
`;
+
+const loadingAriaLabel = i18n.translate(
+ 'xpack.infra.logs.analysis.anomaliesSectionLoadingAriaLabel',
+ { defaultMessage: 'Loading anomalies' }
+);
+
+const LoadingOverlayContent = () => ;
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx
index 96f665b3693ca..2965e1fede822 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx
@@ -28,7 +28,7 @@ import { useLinkProps } from '../../../../../hooks/use_link_props';
import { TimeRange } from '../../../../../../common/http_api/shared/time_range';
import { partitionField } from '../../../../../../common/log_analysis/job_parameters';
import { getEntitySpecificSingleMetricViewerLink } from '../../../../../components/logging/log_analysis_results/analyze_in_ml_button';
-import { LogEntryRateExample } from '../../../../../../common/http_api/log_analysis/results';
+import { LogEntryExample } from '../../../../../../common/http_api/log_analysis/results';
import {
LogColumnConfiguration,
isTimestampLogColumnConfiguration,
@@ -36,6 +36,7 @@ import {
isMessageLogColumnConfiguration,
} from '../../../../../utils/source_configuration';
import { localizedDate } from '../../../../../../common/formatters/datetime';
+import { LogEntryAnomaly } from '../../../../../../common/http_api';
export const exampleMessageScale = 'medium' as const;
export const exampleTimestampFormat = 'time' as const;
@@ -58,19 +59,19 @@ const VIEW_ANOMALY_IN_ML_LABEL = i18n.translate(
}
);
-type Props = LogEntryRateExample & {
+type Props = LogEntryExample & {
timeRange: TimeRange;
- jobId: string;
+ anomaly: LogEntryAnomaly;
};
-export const LogEntryRateExampleMessage: React.FunctionComponent = ({
+export const LogEntryExampleMessage: React.FunctionComponent = ({
id,
dataset,
message,
timestamp,
tiebreaker,
timeRange,
- jobId,
+ anomaly,
}) => {
const [isHovered, setIsHovered] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
@@ -107,8 +108,9 @@ export const LogEntryRateExampleMessage: React.FunctionComponent = ({
});
const viewAnomalyInMachineLearningLinkProps = useLinkProps(
- getEntitySpecificSingleMetricViewerLink(jobId, timeRange, {
+ getEntitySpecificSingleMetricViewerLink(anomaly.jobId, timeRange, {
[partitionField]: dataset,
+ ...(anomaly.categoryId ? { mlcategory: anomaly.categoryId } : {}),
})
);
@@ -233,11 +235,11 @@ export const exampleMessageColumnConfigurations: LogColumnConfiguration[] = [
},
];
-export const LogEntryRateExampleMessageHeaders: React.FunctionComponent<{
+export const LogEntryExampleMessageHeaders: React.FunctionComponent<{
dateTime: number;
}> = ({ dateTime }) => {
return (
-
+
<>
{exampleMessageColumnConfigurations.map((columnConfiguration) => {
if (isTimestampLogColumnConfiguration(columnConfiguration)) {
@@ -280,11 +282,11 @@ export const LogEntryRateExampleMessageHeaders: React.FunctionComponent<{
{null}
>
-
+
);
};
-const LogEntryRateExampleMessageHeadersWrapper = euiStyled(LogColumnHeadersWrapper)`
+const LogEntryExampleMessageHeadersWrapper = euiStyled(LogColumnHeadersWrapper)`
border-bottom: none;
box-shadow: none;
padding-right: 0;
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx
index c70a456bfe06a..e0a3b6fb91db0 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx
@@ -4,45 +4,52 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui';
+import {
+ EuiBasicTable,
+ EuiBasicTableColumn,
+ EuiIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonIcon,
+ EuiSpacer,
+} from '@elastic/eui';
import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services';
import moment from 'moment';
import { i18n } from '@kbn/i18n';
-import React, { useCallback, useMemo, useState } from 'react';
+import React, { useCallback, useMemo } from 'react';
import { useSet } from 'react-use';
import { TimeRange } from '../../../../../../common/http_api/shared/time_range';
import {
formatAnomalyScore,
getFriendlyNameForPartitionId,
+ formatOneDecimalPlace,
} from '../../../../../../common/log_analysis';
+import { AnomalyType } from '../../../../../../common/http_api/log_analysis';
import { RowExpansionButton } from '../../../../../components/basic_table';
-import { LogEntryRateResults } from '../../use_log_entry_rate_results';
import { AnomaliesTableExpandedRow } from './expanded_row';
import { AnomalySeverityIndicator } from '../../../../../components/logging/log_analysis_results/anomaly_severity_indicator';
import { useKibanaUiSetting } from '../../../../../utils/use_kibana_ui_setting';
+import {
+ Page,
+ FetchNextPage,
+ FetchPreviousPage,
+ ChangeSortOptions,
+ ChangePaginationOptions,
+ SortOptions,
+ PaginationOptions,
+ LogEntryAnomalies,
+} from '../../use_log_entry_anomalies_results';
+import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay_wrapper';
interface TableItem {
id: string;
dataset: string;
datasetName: string;
anomalyScore: number;
- anomalyMessage: string;
startTime: number;
-}
-
-interface SortingOptions {
- sort: {
- field: keyof TableItem;
- direction: 'asc' | 'desc';
- };
-}
-
-interface PaginationOptions {
- pageIndex: number;
- pageSize: number;
- totalItemCount: number;
- pageSizeOptions: number[];
- hidePerPageOptions: boolean;
+ typical: number;
+ actual: number;
+ type: AnomalyType;
}
const anomalyScoreColumnName = i18n.translate(
@@ -73,125 +80,78 @@ const datasetColumnName = i18n.translate(
}
);
-const moreThanExpectedAnomalyMessage = i18n.translate(
- 'xpack.infra.logs.analysis.anomaliesTableMoreThanExpectedAnomalyMessage',
- {
- defaultMessage: 'More log messages in this dataset than expected',
- }
-);
-
-const fewerThanExpectedAnomalyMessage = i18n.translate(
- 'xpack.infra.logs.analysis.anomaliesTableFewerThanExpectedAnomalyMessage',
- {
- defaultMessage: 'Fewer log messages in this dataset than expected',
- }
-);
-
-const getAnomalyMessage = (actualRate: number, typicalRate: number): string => {
- return actualRate < typicalRate
- ? fewerThanExpectedAnomalyMessage
- : moreThanExpectedAnomalyMessage;
-};
-
export const AnomaliesTable: React.FunctionComponent<{
- results: LogEntryRateResults;
+ results: LogEntryAnomalies;
setTimeRange: (timeRange: TimeRange) => void;
timeRange: TimeRange;
- jobId: string;
-}> = ({ results, timeRange, setTimeRange, jobId }) => {
+ changeSortOptions: ChangeSortOptions;
+ changePaginationOptions: ChangePaginationOptions;
+ sortOptions: SortOptions;
+ paginationOptions: PaginationOptions;
+ page: Page;
+ fetchNextPage?: FetchNextPage;
+ fetchPreviousPage?: FetchPreviousPage;
+ isLoading: boolean;
+}> = ({
+ results,
+ timeRange,
+ setTimeRange,
+ changeSortOptions,
+ sortOptions,
+ changePaginationOptions,
+ paginationOptions,
+ fetchNextPage,
+ fetchPreviousPage,
+ page,
+ isLoading,
+}) => {
const [dateFormat] = useKibanaUiSetting('dateFormat', 'Y-MM-DD HH:mm:ss');
+ const tableSortOptions = useMemo(() => {
+ return {
+ sort: sortOptions,
+ };
+ }, [sortOptions]);
+
const tableItems: TableItem[] = useMemo(() => {
- return results.anomalies.map((anomaly) => {
+ return results.map((anomaly) => {
return {
id: anomaly.id,
- dataset: anomaly.partitionId,
- datasetName: getFriendlyNameForPartitionId(anomaly.partitionId),
+ dataset: anomaly.dataset,
+ datasetName: getFriendlyNameForPartitionId(anomaly.dataset),
anomalyScore: formatAnomalyScore(anomaly.anomalyScore),
- anomalyMessage: getAnomalyMessage(anomaly.actualLogEntryRate, anomaly.typicalLogEntryRate),
startTime: anomaly.startTime,
+ type: anomaly.type,
+ typical: anomaly.typical,
+ actual: anomaly.actual,
};
});
}, [results]);
const [expandedIds, { add: expandId, remove: collapseId }] = useSet(new Set());
- const expandedDatasetRowContents = useMemo(
+ const expandedIdsRowContents = useMemo(
() =>
- [...expandedIds].reduce>((aggregatedDatasetRows, id) => {
- const anomaly = results.anomalies.find((_anomaly) => _anomaly.id === id);
+ [...expandedIds].reduce>((aggregatedRows, id) => {
+ const anomaly = results.find((_anomaly) => _anomaly.id === id);
return {
- ...aggregatedDatasetRows,
+ ...aggregatedRows,
[id]: anomaly ? (
-
+
) : null,
};
}, {}),
- [expandedIds, results, timeRange, jobId]
+ [expandedIds, results, timeRange]
);
- const [sorting, setSorting] = useState({
- sort: {
- field: 'anomalyScore',
- direction: 'desc',
- },
- });
-
- const [_pagination, setPagination] = useState({
- pageIndex: 0,
- pageSize: 20,
- totalItemCount: results.anomalies.length,
- pageSizeOptions: [10, 20, 50],
- hidePerPageOptions: false,
- });
-
- const paginationOptions = useMemo(() => {
- return {
- ..._pagination,
- totalItemCount: results.anomalies.length,
- };
- }, [_pagination, results]);
-
const handleTableChange = useCallback(
- ({ page = {}, sort = {} }) => {
- const { index, size } = page;
- setPagination((currentPagination) => {
- return {
- ...currentPagination,
- pageIndex: index,
- pageSize: size,
- };
- });
- const { field, direction } = sort;
- setSorting({
- sort: {
- field,
- direction,
- },
- });
+ ({ sort = {} }) => {
+ changeSortOptions(sort);
},
- [setSorting, setPagination]
+ [changeSortOptions]
);
- const sortedTableItems = useMemo(() => {
- let sortedItems: TableItem[] = [];
- if (sorting.sort.field === 'datasetName') {
- sortedItems = tableItems.sort((a, b) => (a.datasetName > b.datasetName ? 1 : -1));
- } else if (sorting.sort.field === 'anomalyScore') {
- sortedItems = tableItems.sort((a, b) => a.anomalyScore - b.anomalyScore);
- } else if (sorting.sort.field === 'startTime') {
- sortedItems = tableItems.sort((a, b) => a.startTime - b.startTime);
- }
-
- return sorting.sort.direction === 'asc' ? sortedItems : sortedItems.reverse();
- }, [tableItems, sorting]);
-
- const pageOfItems: TableItem[] = useMemo(() => {
- const { pageIndex, pageSize } = paginationOptions;
- return sortedTableItems.slice(pageIndex * pageSize, pageIndex * pageSize + pageSize);
- }, [paginationOptions, sortedTableItems]);
-
const columns: Array> = useMemo(
() => [
{
@@ -204,10 +164,11 @@ export const AnomaliesTable: React.FunctionComponent<{
render: (anomalyScore: number) => ,
},
{
- field: 'anomalyMessage',
name: anomalyMessageColumnName,
- sortable: false,
truncateText: true,
+ render: (item: TableItem) => (
+
+ ),
},
{
field: 'startTime',
@@ -240,18 +201,116 @@ export const AnomaliesTable: React.FunctionComponent<{
],
[collapseId, expandId, expandedIds, dateFormat]
);
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+};
+
+const AnomalyMessage = ({
+ actual,
+ typical,
+ type,
+}: {
+ actual: number;
+ typical: number;
+ type: AnomalyType;
+}) => {
+ const moreThanExpectedAnomalyMessage = i18n.translate(
+ 'xpack.infra.logs.analysis.anomaliesTableMoreThanExpectedAnomalyMessage',
+ {
+ defaultMessage:
+ 'more log messages in this {type, select, logRate {dataset} logCategory {category}} than expected',
+ values: { type },
+ }
+ );
+
+ const fewerThanExpectedAnomalyMessage = i18n.translate(
+ 'xpack.infra.logs.analysis.anomaliesTableFewerThanExpectedAnomalyMessage',
+ {
+ defaultMessage:
+ 'fewer log messages in this {type, select, logRate {dataset} logCategory {category}} than expected',
+ values: { type },
+ }
+ );
+
+ const isMore = actual > typical;
+ const message = isMore ? moreThanExpectedAnomalyMessage : fewerThanExpectedAnomalyMessage;
+ const ratio = isMore ? actual / typical : typical / actual;
+ const icon = isMore ? 'sortUp' : 'sortDown';
+ // Edge case scenarios where actual and typical might sit at 0.
+ const useRatio = ratio !== Infinity;
+ const ratioMessage = useRatio ? `${formatOneDecimalPlace(ratio)}x` : '';
return (
-
+
+ {`${ratioMessage} ${message}`}
+
+ );
+};
+
+const previousPageLabel = i18n.translate(
+ 'xpack.infra.logs.analysis.anomaliesTablePreviousPageLabel',
+ {
+ defaultMessage: 'Previous page',
+ }
+);
+
+const nextPageLabel = i18n.translate('xpack.infra.logs.analysis.anomaliesTableNextPageLabel', {
+ defaultMessage: 'Next page',
+});
+
+const PaginationControls = ({
+ fetchPreviousPage,
+ fetchNextPage,
+ page,
+ isLoading,
+}: {
+ fetchPreviousPage?: () => void;
+ fetchNextPage?: () => void;
+ page: number;
+ isLoading: boolean;
+}) => {
+ return (
+
+
+
+
+
+ {page}
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/log_rate/bar_chart.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/log_rate/bar_chart.tsx
deleted file mode 100644
index 498a9f88176f8..0000000000000
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/log_rate/bar_chart.tsx
+++ /dev/null
@@ -1,100 +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 {
- Axis,
- BarSeries,
- Chart,
- niceTimeFormatter,
- Settings,
- TooltipValue,
- BrushEndListener,
- LIGHT_THEME,
- DARK_THEME,
-} from '@elastic/charts';
-import { i18n } from '@kbn/i18n';
-import numeral from '@elastic/numeral';
-import moment from 'moment';
-import React, { useCallback, useMemo } from 'react';
-
-import { TimeRange } from '../../../../../../common/http_api/shared/time_range';
-import { useKibanaUiSetting } from '../../../../../utils/use_kibana_ui_setting';
-
-export const LogEntryRateBarChart: React.FunctionComponent<{
- setTimeRange: (timeRange: TimeRange) => void;
- timeRange: TimeRange;
- series: Array<{ group: string; time: number; value: number }>;
-}> = ({ series, setTimeRange, timeRange }) => {
- const [dateFormat] = useKibanaUiSetting('dateFormat');
- const [isDarkMode] = useKibanaUiSetting('theme:darkMode');
-
- const chartDateFormatter = useMemo(
- () => niceTimeFormatter([timeRange.startTime, timeRange.endTime]),
- [timeRange]
- );
-
- const tooltipProps = useMemo(
- () => ({
- headerFormatter: (tooltipData: TooltipValue) =>
- moment(tooltipData.value).format(dateFormat || 'Y-MM-DD HH:mm:ss.SSS'),
- }),
- [dateFormat]
- );
-
- const handleBrushEnd = useCallback(
- ({ x }) => {
- if (!x) {
- return;
- }
- const [startTime, endTime] = x;
- setTimeRange({
- endTime,
- startTime,
- });
- },
- [setTimeRange]
- );
-
- return (
-
-
-
- numeral(value.toPrecision(3)).format('0[.][00]a')} // https://github.com/adamwdraper/Numeral-js/issues/194
- />
-
-
-
-
- );
-};
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/log_rate/index.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/log_rate/index.tsx
deleted file mode 100644
index 3da025d90119f..0000000000000
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/log_rate/index.tsx
+++ /dev/null
@@ -1,98 +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 { EuiEmptyPrompt, EuiLoadingSpinner, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import React, { useMemo } from 'react';
-
-import { TimeRange } from '../../../../../../common/http_api/shared/time_range';
-import { BetaBadge } from '../../../../../components/beta_badge';
-import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay_wrapper';
-import { LogEntryRateResults as Results } from '../../use_log_entry_rate_results';
-import { getLogEntryRatePartitionedSeries } from '../helpers/data_formatters';
-import { LogEntryRateBarChart } from './bar_chart';
-
-export const LogRateResults = ({
- isLoading,
- results,
- setTimeRange,
- timeRange,
-}: {
- isLoading: boolean;
- results: Results | null;
- setTimeRange: (timeRange: TimeRange) => void;
- timeRange: TimeRange;
-}) => {
- const logEntryRateSeries = useMemo(
- () => (results && results.histogramBuckets ? getLogEntryRatePartitionedSeries(results) : []),
- [results]
- );
-
- return (
- <>
-
-
- {title}
-
-
- }>
- {!results || (results && results.histogramBuckets && !results.histogramBuckets.length) ? (
- <>
-
-
- {i18n.translate('xpack.infra.logs.analysis.logRateSectionNoDataTitle', {
- defaultMessage: 'There is no data to display.',
- })}
-
- }
- titleSize="m"
- body={
-
- {i18n.translate('xpack.infra.logs.analysis.logRateSectionNoDataBody', {
- defaultMessage: 'You may want to adjust your time range.',
- })}
-
- }
- />
- >
- ) : (
- <>
-
-
-
- {i18n.translate('xpack.infra.logs.analysis.logRateSectionBucketSpanLabel', {
- defaultMessage: 'Bucket span: ',
- })}
-
- {i18n.translate('xpack.infra.logs.analysis.logRateSectionBucketSpanValue', {
- defaultMessage: '15 minutes',
- })}
-
-
-
- >
- )}
-
- >
- );
-};
-
-const title = i18n.translate('xpack.infra.logs.analysis.logRateSectionTitle', {
- defaultMessage: 'Log entries',
-});
-
-const loadingAriaLabel = i18n.translate(
- 'xpack.infra.logs.analysis.logRateSectionLoadingAriaLabel',
- { defaultMessage: 'Loading log rate results' }
-);
-
-const LoadingOverlayContent = () => ;
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_anomalies.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_anomalies.ts
new file mode 100644
index 0000000000000..d4a0eaae43ac0
--- /dev/null
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_anomalies.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 { npStart } from '../../../../legacy_singletons';
+import {
+ getLogEntryAnomaliesRequestPayloadRT,
+ getLogEntryAnomaliesSuccessReponsePayloadRT,
+ LOG_ANALYSIS_GET_LOG_ENTRY_ANOMALIES_PATH,
+} from '../../../../../common/http_api/log_analysis';
+import { decodeOrThrow } from '../../../../../common/runtime_types';
+import { Sort, Pagination } from '../../../../../common/http_api/log_analysis';
+
+export const callGetLogEntryAnomaliesAPI = async (
+ sourceId: string,
+ startTime: number,
+ endTime: number,
+ sort: Sort,
+ pagination: Pagination
+) => {
+ const response = await npStart.http.fetch(LOG_ANALYSIS_GET_LOG_ENTRY_ANOMALIES_PATH, {
+ method: 'POST',
+ body: JSON.stringify(
+ getLogEntryAnomaliesRequestPayloadRT.encode({
+ data: {
+ sourceId,
+ timeRange: {
+ startTime,
+ endTime,
+ },
+ sort,
+ pagination,
+ },
+ })
+ ),
+ });
+
+ return decodeOrThrow(getLogEntryAnomaliesSuccessReponsePayloadRT)(response);
+};
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_rate_examples.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_examples.ts
similarity index 77%
rename from x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_rate_examples.ts
rename to x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_examples.ts
index d3b30da72af96..a125b53f9e635 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_rate_examples.ts
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_examples.ts
@@ -10,23 +10,24 @@ import { identity } from 'fp-ts/lib/function';
import { npStart } from '../../../../legacy_singletons';
import {
- getLogEntryRateExamplesRequestPayloadRT,
- getLogEntryRateExamplesSuccessReponsePayloadRT,
+ getLogEntryExamplesRequestPayloadRT,
+ getLogEntryExamplesSuccessReponsePayloadRT,
LOG_ANALYSIS_GET_LOG_ENTRY_RATE_EXAMPLES_PATH,
} from '../../../../../common/http_api/log_analysis';
import { createPlainError, throwErrors } from '../../../../../common/runtime_types';
-export const callGetLogEntryRateExamplesAPI = async (
+export const callGetLogEntryExamplesAPI = async (
sourceId: string,
startTime: number,
endTime: number,
dataset: string,
- exampleCount: number
+ exampleCount: number,
+ categoryId?: string
) => {
const response = await npStart.http.fetch(LOG_ANALYSIS_GET_LOG_ENTRY_RATE_EXAMPLES_PATH, {
method: 'POST',
body: JSON.stringify(
- getLogEntryRateExamplesRequestPayloadRT.encode({
+ getLogEntryExamplesRequestPayloadRT.encode({
data: {
dataset,
exampleCount,
@@ -35,13 +36,14 @@ export const callGetLogEntryRateExamplesAPI = async (
startTime,
endTime,
},
+ categoryId,
},
})
),
});
return pipe(
- getLogEntryRateExamplesSuccessReponsePayloadRT.decode(response),
+ getLogEntryExamplesSuccessReponsePayloadRT.decode(response),
fold(throwErrors(createPlainError), identity)
);
};
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_anomalies_results.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_anomalies_results.ts
new file mode 100644
index 0000000000000..cadb4c420c133
--- /dev/null
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_anomalies_results.ts
@@ -0,0 +1,262 @@
+/*
+ * 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 { useMemo, useState, useCallback, useEffect, useReducer } from 'react';
+
+import { LogEntryAnomaly } from '../../../../common/http_api';
+import { useTrackedPromise } from '../../../utils/use_tracked_promise';
+import { callGetLogEntryAnomaliesAPI } from './service_calls/get_log_entry_anomalies';
+import { Sort, Pagination, PaginationCursor } from '../../../../common/http_api/log_analysis';
+
+export type SortOptions = Sort;
+export type PaginationOptions = Pick;
+export type Page = number;
+export type FetchNextPage = () => void;
+export type FetchPreviousPage = () => void;
+export type ChangeSortOptions = (sortOptions: Sort) => void;
+export type ChangePaginationOptions = (paginationOptions: PaginationOptions) => void;
+export type LogEntryAnomalies = LogEntryAnomaly[];
+interface PaginationCursors {
+ previousPageCursor: PaginationCursor;
+ nextPageCursor: PaginationCursor;
+}
+
+interface ReducerState {
+ page: number;
+ lastReceivedCursors: PaginationCursors | undefined;
+ paginationCursor: Pagination['cursor'] | undefined;
+ hasNextPage: boolean;
+ paginationOptions: PaginationOptions;
+ sortOptions: Sort;
+ timeRange: {
+ start: number;
+ end: number;
+ };
+}
+
+type ReducerStateDefaults = Pick<
+ ReducerState,
+ 'page' | 'lastReceivedCursors' | 'paginationCursor' | 'hasNextPage'
+>;
+
+type ReducerAction =
+ | { type: 'changePaginationOptions'; payload: { paginationOptions: PaginationOptions } }
+ | { type: 'changeSortOptions'; payload: { sortOptions: Sort } }
+ | { type: 'fetchNextPage' }
+ | { type: 'fetchPreviousPage' }
+ | { type: 'changeHasNextPage'; payload: { hasNextPage: boolean } }
+ | { type: 'changeLastReceivedCursors'; payload: { lastReceivedCursors: PaginationCursors } }
+ | { type: 'changeTimeRange'; payload: { timeRange: { start: number; end: number } } };
+
+const stateReducer = (state: ReducerState, action: ReducerAction): ReducerState => {
+ const resetPagination = {
+ page: 1,
+ paginationCursor: undefined,
+ };
+ switch (action.type) {
+ case 'changePaginationOptions':
+ return {
+ ...state,
+ ...resetPagination,
+ ...action.payload,
+ };
+ case 'changeSortOptions':
+ return {
+ ...state,
+ ...resetPagination,
+ ...action.payload,
+ };
+ case 'changeHasNextPage':
+ return {
+ ...state,
+ ...action.payload,
+ };
+ case 'changeLastReceivedCursors':
+ return {
+ ...state,
+ ...action.payload,
+ };
+ case 'fetchNextPage':
+ return state.lastReceivedCursors
+ ? {
+ ...state,
+ page: state.page + 1,
+ paginationCursor: { searchAfter: state.lastReceivedCursors.nextPageCursor },
+ }
+ : state;
+ case 'fetchPreviousPage':
+ return state.lastReceivedCursors
+ ? {
+ ...state,
+ page: state.page - 1,
+ paginationCursor: { searchBefore: state.lastReceivedCursors.previousPageCursor },
+ }
+ : state;
+ case 'changeTimeRange':
+ return {
+ ...state,
+ ...resetPagination,
+ ...action.payload,
+ };
+ default:
+ return state;
+ }
+};
+
+const STATE_DEFAULTS: ReducerStateDefaults = {
+ // NOTE: This piece of state is purely for the client side, it could be extracted out of the hook.
+ page: 1,
+ // Cursor from the last request
+ lastReceivedCursors: undefined,
+ // Cursor to use for the next request. For the first request, and therefore not paging, this will be undefined.
+ paginationCursor: undefined,
+ hasNextPage: false,
+};
+
+export const useLogEntryAnomaliesResults = ({
+ endTime,
+ startTime,
+ sourceId,
+ defaultSortOptions,
+ defaultPaginationOptions,
+}: {
+ endTime: number;
+ startTime: number;
+ sourceId: string;
+ defaultSortOptions: Sort;
+ defaultPaginationOptions: Pick;
+}) => {
+ const initStateReducer = (stateDefaults: ReducerStateDefaults): ReducerState => {
+ return {
+ ...stateDefaults,
+ paginationOptions: defaultPaginationOptions,
+ sortOptions: defaultSortOptions,
+ timeRange: {
+ start: startTime,
+ end: endTime,
+ },
+ };
+ };
+
+ const [reducerState, dispatch] = useReducer(stateReducer, STATE_DEFAULTS, initStateReducer);
+
+ const [logEntryAnomalies, setLogEntryAnomalies] = useState([]);
+
+ const [getLogEntryAnomaliesRequest, getLogEntryAnomalies] = useTrackedPromise(
+ {
+ cancelPreviousOn: 'creation',
+ createPromise: async () => {
+ const {
+ timeRange: { start: queryStartTime, end: queryEndTime },
+ sortOptions,
+ paginationOptions,
+ paginationCursor,
+ } = reducerState;
+ return await callGetLogEntryAnomaliesAPI(
+ sourceId,
+ queryStartTime,
+ queryEndTime,
+ sortOptions,
+ {
+ ...paginationOptions,
+ cursor: paginationCursor,
+ }
+ );
+ },
+ onResolve: ({ data: { anomalies, paginationCursors: requestCursors, hasMoreEntries } }) => {
+ const { paginationCursor } = reducerState;
+ if (requestCursors) {
+ dispatch({
+ type: 'changeLastReceivedCursors',
+ payload: { lastReceivedCursors: requestCursors },
+ });
+ }
+ // Check if we have more "next" entries. "Page" covers the "previous" scenario,
+ // since we need to know the page we're on anyway.
+ if (!paginationCursor || (paginationCursor && 'searchAfter' in paginationCursor)) {
+ dispatch({ type: 'changeHasNextPage', payload: { hasNextPage: hasMoreEntries } });
+ } else if (paginationCursor && 'searchBefore' in paginationCursor) {
+ // We've requested a previous page, therefore there is a next page.
+ dispatch({ type: 'changeHasNextPage', payload: { hasNextPage: true } });
+ }
+ setLogEntryAnomalies(anomalies);
+ },
+ },
+ [
+ sourceId,
+ dispatch,
+ reducerState.timeRange,
+ reducerState.sortOptions,
+ reducerState.paginationOptions,
+ reducerState.paginationCursor,
+ ]
+ );
+
+ const changeSortOptions = useCallback(
+ (nextSortOptions: Sort) => {
+ dispatch({ type: 'changeSortOptions', payload: { sortOptions: nextSortOptions } });
+ },
+ [dispatch]
+ );
+
+ const changePaginationOptions = useCallback(
+ (nextPaginationOptions: PaginationOptions) => {
+ dispatch({
+ type: 'changePaginationOptions',
+ payload: { paginationOptions: nextPaginationOptions },
+ });
+ },
+ [dispatch]
+ );
+
+ // Time range has changed
+ useEffect(() => {
+ dispatch({
+ type: 'changeTimeRange',
+ payload: { timeRange: { start: startTime, end: endTime } },
+ });
+ }, [startTime, endTime]);
+
+ useEffect(() => {
+ getLogEntryAnomalies();
+ }, [getLogEntryAnomalies]);
+
+ const handleFetchNextPage = useCallback(() => {
+ if (reducerState.lastReceivedCursors) {
+ dispatch({ type: 'fetchNextPage' });
+ }
+ }, [dispatch, reducerState]);
+
+ const handleFetchPreviousPage = useCallback(() => {
+ if (reducerState.lastReceivedCursors) {
+ dispatch({ type: 'fetchPreviousPage' });
+ }
+ }, [dispatch, reducerState]);
+
+ const isLoadingLogEntryAnomalies = useMemo(
+ () => getLogEntryAnomaliesRequest.state === 'pending',
+ [getLogEntryAnomaliesRequest.state]
+ );
+
+ const hasFailedLoadingLogEntryAnomalies = useMemo(
+ () => getLogEntryAnomaliesRequest.state === 'rejected',
+ [getLogEntryAnomaliesRequest.state]
+ );
+
+ return {
+ logEntryAnomalies,
+ getLogEntryAnomalies,
+ isLoadingLogEntryAnomalies,
+ hasFailedLoadingLogEntryAnomalies,
+ changeSortOptions,
+ sortOptions: reducerState.sortOptions,
+ changePaginationOptions,
+ paginationOptions: reducerState.paginationOptions,
+ fetchPreviousPage: reducerState.page > 1 ? handleFetchPreviousPage : undefined,
+ fetchNextPage: reducerState.hasNextPage ? handleFetchNextPage : undefined,
+ page: reducerState.page,
+ };
+};
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_examples.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_examples.ts
new file mode 100644
index 0000000000000..fae5bd200a415
--- /dev/null
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_examples.ts
@@ -0,0 +1,65 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { useMemo, useState } from 'react';
+
+import { LogEntryExample } from '../../../../common/http_api';
+import { useTrackedPromise } from '../../../utils/use_tracked_promise';
+import { callGetLogEntryExamplesAPI } from './service_calls/get_log_entry_examples';
+
+export const useLogEntryExamples = ({
+ dataset,
+ endTime,
+ exampleCount,
+ sourceId,
+ startTime,
+ categoryId,
+}: {
+ dataset: string;
+ endTime: number;
+ exampleCount: number;
+ sourceId: string;
+ startTime: number;
+ categoryId?: string;
+}) => {
+ const [logEntryExamples, setLogEntryExamples] = useState([]);
+
+ const [getLogEntryExamplesRequest, getLogEntryExamples] = useTrackedPromise(
+ {
+ cancelPreviousOn: 'creation',
+ createPromise: async () => {
+ return await callGetLogEntryExamplesAPI(
+ sourceId,
+ startTime,
+ endTime,
+ dataset,
+ exampleCount,
+ categoryId
+ );
+ },
+ onResolve: ({ data: { examples } }) => {
+ setLogEntryExamples(examples);
+ },
+ },
+ [dataset, endTime, exampleCount, sourceId, startTime]
+ );
+
+ const isLoadingLogEntryExamples = useMemo(() => getLogEntryExamplesRequest.state === 'pending', [
+ getLogEntryExamplesRequest.state,
+ ]);
+
+ const hasFailedLoadingLogEntryExamples = useMemo(
+ () => getLogEntryExamplesRequest.state === 'rejected',
+ [getLogEntryExamplesRequest.state]
+ );
+
+ return {
+ getLogEntryExamples,
+ hasFailedLoadingLogEntryExamples,
+ isLoadingLogEntryExamples,
+ logEntryExamples,
+ };
+};
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_examples.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_examples.ts
deleted file mode 100644
index 12bcdb2a4b4d6..0000000000000
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_examples.ts
+++ /dev/null
@@ -1,63 +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 { useMemo, useState } from 'react';
-
-import { LogEntryRateExample } from '../../../../common/http_api';
-import { useTrackedPromise } from '../../../utils/use_tracked_promise';
-import { callGetLogEntryRateExamplesAPI } from './service_calls/get_log_entry_rate_examples';
-
-export const useLogEntryRateExamples = ({
- dataset,
- endTime,
- exampleCount,
- sourceId,
- startTime,
-}: {
- dataset: string;
- endTime: number;
- exampleCount: number;
- sourceId: string;
- startTime: number;
-}) => {
- const [logEntryRateExamples, setLogEntryRateExamples] = useState([]);
-
- const [getLogEntryRateExamplesRequest, getLogEntryRateExamples] = useTrackedPromise(
- {
- cancelPreviousOn: 'creation',
- createPromise: async () => {
- return await callGetLogEntryRateExamplesAPI(
- sourceId,
- startTime,
- endTime,
- dataset,
- exampleCount
- );
- },
- onResolve: ({ data: { examples } }) => {
- setLogEntryRateExamples(examples);
- },
- },
- [dataset, endTime, exampleCount, sourceId, startTime]
- );
-
- const isLoadingLogEntryRateExamples = useMemo(
- () => getLogEntryRateExamplesRequest.state === 'pending',
- [getLogEntryRateExamplesRequest.state]
- );
-
- const hasFailedLoadingLogEntryRateExamples = useMemo(
- () => getLogEntryRateExamplesRequest.state === 'rejected',
- [getLogEntryRateExamplesRequest.state]
- );
-
- return {
- getLogEntryRateExamples,
- hasFailedLoadingLogEntryRateExamples,
- isLoadingLogEntryRateExamples,
- logEntryRateExamples,
- };
-};
diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts
index 8af37a36ef745..6596e07ebaca5 100644
--- a/x-pack/plugins/infra/server/infra_server.ts
+++ b/x-pack/plugins/infra/server/infra_server.ts
@@ -15,9 +15,10 @@ import {
initGetLogEntryCategoryDatasetsRoute,
initGetLogEntryCategoryExamplesRoute,
initGetLogEntryRateRoute,
- initGetLogEntryRateExamplesRoute,
+ initGetLogEntryExamplesRoute,
initValidateLogAnalysisDatasetsRoute,
initValidateLogAnalysisIndicesRoute,
+ initGetLogEntryAnomaliesRoute,
} from './routes/log_analysis';
import { initMetricExplorerRoute } from './routes/metrics_explorer';
import { initMetadataRoute } from './routes/metadata';
@@ -51,13 +52,14 @@ export const initInfraServer = (libs: InfraBackendLibs) => {
initGetLogEntryCategoryDatasetsRoute(libs);
initGetLogEntryCategoryExamplesRoute(libs);
initGetLogEntryRateRoute(libs);
+ initGetLogEntryAnomaliesRoute(libs);
initSnapshotRoute(libs);
initNodeDetailsRoute(libs);
initSourceRoute(libs);
initValidateLogAnalysisDatasetsRoute(libs);
initValidateLogAnalysisIndicesRoute(libs);
initLogEntriesRoute(libs);
- initGetLogEntryRateExamplesRoute(libs);
+ initGetLogEntryExamplesRoute(libs);
initLogEntriesHighlightsRoute(libs);
initLogEntriesSummaryRoute(libs);
initLogEntriesSummaryHighlightsRoute(libs);
diff --git a/x-pack/plugins/infra/server/lib/log_analysis/common.ts b/x-pack/plugins/infra/server/lib/log_analysis/common.ts
new file mode 100644
index 0000000000000..0c0b0a0f19982
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/log_analysis/common.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import type { MlAnomalyDetectors } from '../../types';
+import { startTracingSpan } from '../../../common/performance_tracing';
+import { NoLogAnalysisMlJobError } from './errors';
+
+export async function fetchMlJob(mlAnomalyDetectors: MlAnomalyDetectors, jobId: string) {
+ const finalizeMlGetJobSpan = startTracingSpan('Fetch ml job from ES');
+ const {
+ jobs: [mlJob],
+ } = await mlAnomalyDetectors.jobs(jobId);
+
+ const mlGetJobSpan = finalizeMlGetJobSpan();
+
+ if (mlJob == null) {
+ throw new NoLogAnalysisMlJobError(`Failed to find ml job ${jobId}.`);
+ }
+
+ return {
+ mlJob,
+ timing: {
+ spans: [mlGetJobSpan],
+ },
+ };
+}
diff --git a/x-pack/plugins/infra/server/lib/log_analysis/errors.ts b/x-pack/plugins/infra/server/lib/log_analysis/errors.ts
index e07126416f4ce..09fee8844fbc5 100644
--- a/x-pack/plugins/infra/server/lib/log_analysis/errors.ts
+++ b/x-pack/plugins/infra/server/lib/log_analysis/errors.ts
@@ -33,3 +33,10 @@ export class UnknownCategoryError extends Error {
Object.setPrototypeOf(this, new.target.prototype);
}
}
+
+export class InsufficientAnomalyMlJobsConfigured extends Error {
+ constructor(message?: string) {
+ super(message);
+ Object.setPrototypeOf(this, new.target.prototype);
+ }
+}
diff --git a/x-pack/plugins/infra/server/lib/log_analysis/index.ts b/x-pack/plugins/infra/server/lib/log_analysis/index.ts
index 44c2bafce4194..c9a176be0a28f 100644
--- a/x-pack/plugins/infra/server/lib/log_analysis/index.ts
+++ b/x-pack/plugins/infra/server/lib/log_analysis/index.ts
@@ -7,3 +7,4 @@
export * from './errors';
export * from './log_entry_categories_analysis';
export * from './log_entry_rate_analysis';
+export * from './log_entry_anomalies';
diff --git a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts
new file mode 100644
index 0000000000000..12ae516564d66
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts
@@ -0,0 +1,398 @@
+/*
+ * 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 { RequestHandlerContext } from 'src/core/server';
+import { InfraRequestHandlerContext } from '../../types';
+import { TracingSpan, startTracingSpan } from '../../../common/performance_tracing';
+import { fetchMlJob } from './common';
+import {
+ getJobId,
+ logEntryCategoriesJobTypes,
+ logEntryRateJobTypes,
+ jobCustomSettingsRT,
+} from '../../../common/log_analysis';
+import { Sort, Pagination } from '../../../common/http_api/log_analysis';
+import type { MlSystem } from '../../types';
+import { createLogEntryAnomaliesQuery, logEntryAnomaliesResponseRT } from './queries';
+import {
+ InsufficientAnomalyMlJobsConfigured,
+ InsufficientLogAnalysisMlJobConfigurationError,
+ UnknownCategoryError,
+} from './errors';
+import { decodeOrThrow } from '../../../common/runtime_types';
+import {
+ createLogEntryExamplesQuery,
+ logEntryExamplesResponseRT,
+} from './queries/log_entry_examples';
+import { InfraSource } from '../sources';
+import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter';
+import { fetchLogEntryCategories } from './log_entry_categories_analysis';
+
+interface MappedAnomalyHit {
+ id: string;
+ anomalyScore: number;
+ dataset: string;
+ typical: number;
+ actual: number;
+ jobId: string;
+ startTime: number;
+ duration: number;
+ categoryId?: string;
+}
+
+export async function getLogEntryAnomalies(
+ context: RequestHandlerContext & { infra: Required },
+ sourceId: string,
+ startTime: number,
+ endTime: number,
+ sort: Sort,
+ pagination: Pagination
+) {
+ const finalizeLogEntryAnomaliesSpan = startTracingSpan('get log entry anomalies');
+
+ const logRateJobId = getJobId(context.infra.spaceId, sourceId, logEntryRateJobTypes[0]);
+ const logCategoriesJobId = getJobId(
+ context.infra.spaceId,
+ sourceId,
+ logEntryCategoriesJobTypes[0]
+ );
+
+ const jobIds: string[] = [];
+ let jobSpans: TracingSpan[] = [];
+
+ try {
+ const {
+ timing: { spans },
+ } = await fetchMlJob(context.infra.mlAnomalyDetectors, logRateJobId);
+ jobIds.push(logRateJobId);
+ jobSpans = [...jobSpans, ...spans];
+ } catch (e) {
+ // Job wasn't found
+ }
+
+ try {
+ const {
+ timing: { spans },
+ } = await fetchMlJob(context.infra.mlAnomalyDetectors, logCategoriesJobId);
+ jobIds.push(logCategoriesJobId);
+ jobSpans = [...jobSpans, ...spans];
+ } catch (e) {
+ // Job wasn't found
+ }
+
+ if (jobIds.length === 0) {
+ throw new InsufficientAnomalyMlJobsConfigured(
+ 'Log rate or categorisation ML jobs need to be configured to search anomalies'
+ );
+ }
+
+ const {
+ anomalies,
+ paginationCursors,
+ hasMoreEntries,
+ timing: { spans: fetchLogEntryAnomaliesSpans },
+ } = await fetchLogEntryAnomalies(
+ context.infra.mlSystem,
+ jobIds,
+ startTime,
+ endTime,
+ sort,
+ pagination
+ );
+
+ const data = anomalies.map((anomaly) => {
+ const { jobId } = anomaly;
+
+ if (jobId === logRateJobId) {
+ return parseLogRateAnomalyResult(anomaly, logRateJobId);
+ } else {
+ return parseCategoryAnomalyResult(anomaly, logCategoriesJobId);
+ }
+ });
+
+ const logEntryAnomaliesSpan = finalizeLogEntryAnomaliesSpan();
+
+ return {
+ data,
+ paginationCursors,
+ hasMoreEntries,
+ timing: {
+ spans: [logEntryAnomaliesSpan, ...jobSpans, ...fetchLogEntryAnomaliesSpans],
+ },
+ };
+}
+
+const parseLogRateAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => {
+ const {
+ id,
+ anomalyScore,
+ dataset,
+ typical,
+ actual,
+ duration,
+ startTime: anomalyStartTime,
+ } = anomaly;
+
+ return {
+ id,
+ anomalyScore,
+ dataset,
+ typical,
+ actual,
+ duration,
+ startTime: anomalyStartTime,
+ type: 'logRate' as const,
+ jobId,
+ };
+};
+
+const parseCategoryAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => {
+ const {
+ id,
+ anomalyScore,
+ dataset,
+ typical,
+ actual,
+ duration,
+ startTime: anomalyStartTime,
+ categoryId,
+ } = anomaly;
+
+ return {
+ id,
+ anomalyScore,
+ dataset,
+ typical,
+ actual,
+ duration,
+ startTime: anomalyStartTime,
+ categoryId,
+ type: 'logCategory' as const,
+ jobId,
+ };
+};
+
+async function fetchLogEntryAnomalies(
+ mlSystem: MlSystem,
+ jobIds: string[],
+ startTime: number,
+ endTime: number,
+ sort: Sort,
+ pagination: Pagination
+) {
+ // We'll request 1 extra entry on top of our pageSize to determine if there are
+ // more entries to be fetched. This avoids scenarios where the client side can't
+ // determine if entries.length === pageSize actually means there are more entries / next page
+ // or not.
+ const expandedPagination = { ...pagination, pageSize: pagination.pageSize + 1 };
+
+ const finalizeFetchLogEntryAnomaliesSpan = startTracingSpan('fetch log entry anomalies');
+
+ const results = decodeOrThrow(logEntryAnomaliesResponseRT)(
+ await mlSystem.mlAnomalySearch(
+ createLogEntryAnomaliesQuery(jobIds, startTime, endTime, sort, expandedPagination)
+ )
+ );
+
+ const {
+ hits: { hits },
+ } = results;
+ const hasMoreEntries = hits.length > pagination.pageSize;
+
+ // An extra entry was found and hasMoreEntries has been determined, the extra entry can be removed.
+ if (hasMoreEntries) {
+ hits.pop();
+ }
+
+ // To "search_before" the sort order will have been reversed for ES.
+ // The results are now reversed back, to match the requested sort.
+ if (pagination.cursor && 'searchBefore' in pagination.cursor) {
+ hits.reverse();
+ }
+
+ const paginationCursors =
+ hits.length > 0
+ ? {
+ previousPageCursor: hits[0].sort,
+ nextPageCursor: hits[hits.length - 1].sort,
+ }
+ : undefined;
+
+ const anomalies = hits.map((result) => {
+ const {
+ job_id,
+ record_score: anomalyScore,
+ typical,
+ actual,
+ partition_field_value: dataset,
+ bucket_span: duration,
+ timestamp: anomalyStartTime,
+ by_field_value: categoryId,
+ } = result._source;
+
+ return {
+ id: result._id,
+ anomalyScore,
+ dataset,
+ typical: typical[0],
+ actual: actual[0],
+ jobId: job_id,
+ startTime: anomalyStartTime,
+ duration: duration * 1000,
+ categoryId,
+ };
+ });
+
+ const fetchLogEntryAnomaliesSpan = finalizeFetchLogEntryAnomaliesSpan();
+
+ return {
+ anomalies,
+ paginationCursors,
+ hasMoreEntries,
+ timing: {
+ spans: [fetchLogEntryAnomaliesSpan],
+ },
+ };
+}
+
+export async function getLogEntryExamples(
+ context: RequestHandlerContext & { infra: Required },
+ sourceId: string,
+ startTime: number,
+ endTime: number,
+ dataset: string,
+ exampleCount: number,
+ sourceConfiguration: InfraSource,
+ callWithRequest: KibanaFramework['callWithRequest'],
+ categoryId?: string
+) {
+ const finalizeLogEntryExamplesSpan = startTracingSpan('get log entry rate example log entries');
+
+ const jobId = getJobId(
+ context.infra.spaceId,
+ sourceId,
+ categoryId != null ? logEntryCategoriesJobTypes[0] : logEntryRateJobTypes[0]
+ );
+
+ const {
+ mlJob,
+ timing: { spans: fetchMlJobSpans },
+ } = await fetchMlJob(context.infra.mlAnomalyDetectors, jobId);
+
+ const customSettings = decodeOrThrow(jobCustomSettingsRT)(mlJob.custom_settings);
+ const indices = customSettings?.logs_source_config?.indexPattern;
+ const timestampField = customSettings?.logs_source_config?.timestampField;
+ const tiebreakerField = sourceConfiguration.configuration.fields.tiebreaker;
+
+ if (indices == null || timestampField == null) {
+ throw new InsufficientLogAnalysisMlJobConfigurationError(
+ `Failed to find index configuration for ml job ${jobId}`
+ );
+ }
+
+ const {
+ examples,
+ timing: { spans: fetchLogEntryExamplesSpans },
+ } = await fetchLogEntryExamples(
+ context,
+ sourceId,
+ indices,
+ timestampField,
+ tiebreakerField,
+ startTime,
+ endTime,
+ dataset,
+ exampleCount,
+ callWithRequest,
+ categoryId
+ );
+
+ const logEntryExamplesSpan = finalizeLogEntryExamplesSpan();
+
+ return {
+ data: examples,
+ timing: {
+ spans: [logEntryExamplesSpan, ...fetchMlJobSpans, ...fetchLogEntryExamplesSpans],
+ },
+ };
+}
+
+export async function fetchLogEntryExamples(
+ context: RequestHandlerContext & { infra: Required },
+ sourceId: string,
+ indices: string,
+ timestampField: string,
+ tiebreakerField: string,
+ startTime: number,
+ endTime: number,
+ dataset: string,
+ exampleCount: number,
+ callWithRequest: KibanaFramework['callWithRequest'],
+ categoryId?: string
+) {
+ const finalizeEsSearchSpan = startTracingSpan('Fetch log rate examples from ES');
+
+ let categoryQuery: string | undefined;
+
+ // Examples should be further scoped to a specific ML category
+ if (categoryId) {
+ const parsedCategoryId = parseInt(categoryId, 10);
+
+ const logEntryCategoriesCountJobId = getJobId(
+ context.infra.spaceId,
+ sourceId,
+ logEntryCategoriesJobTypes[0]
+ );
+
+ const { logEntryCategoriesById } = await fetchLogEntryCategories(
+ context,
+ logEntryCategoriesCountJobId,
+ [parsedCategoryId]
+ );
+
+ const category = logEntryCategoriesById[parsedCategoryId];
+
+ if (category == null) {
+ throw new UnknownCategoryError(parsedCategoryId);
+ }
+
+ categoryQuery = category._source.terms;
+ }
+
+ const {
+ hits: { hits },
+ } = decodeOrThrow(logEntryExamplesResponseRT)(
+ await callWithRequest(
+ context,
+ 'search',
+ createLogEntryExamplesQuery(
+ indices,
+ timestampField,
+ tiebreakerField,
+ startTime,
+ endTime,
+ dataset,
+ exampleCount,
+ categoryQuery
+ )
+ )
+ );
+
+ const esSearchSpan = finalizeEsSearchSpan();
+
+ return {
+ examples: hits.map((hit) => ({
+ id: hit._id,
+ dataset: hit._source.event?.dataset ?? '',
+ message: hit._source.message ?? '',
+ timestamp: hit.sort[0],
+ tiebreaker: hit.sort[1],
+ })),
+ timing: {
+ spans: [esSearchSpan],
+ },
+ };
+}
diff --git a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts
index 4f244d724405e..6d00ba56e0e66 100644
--- a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts
+++ b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts
@@ -17,7 +17,6 @@ import { decodeOrThrow } from '../../../common/runtime_types';
import type { MlAnomalyDetectors, MlSystem } from '../../types';
import {
InsufficientLogAnalysisMlJobConfigurationError,
- NoLogAnalysisMlJobError,
NoLogAnalysisResultsIndexError,
UnknownCategoryError,
} from './errors';
@@ -45,6 +44,7 @@ import {
topLogEntryCategoriesResponseRT,
} from './queries/top_log_entry_categories';
import { InfraSource } from '../sources';
+import { fetchMlJob } from './common';
const COMPOSITE_AGGREGATION_BATCH_SIZE = 1000;
@@ -213,7 +213,7 @@ export async function getLogEntryCategoryExamples(
const {
mlJob,
timing: { spans: fetchMlJobSpans },
- } = await fetchMlJob(context, logEntryCategoriesCountJobId);
+ } = await fetchMlJob(context.infra.mlAnomalyDetectors, logEntryCategoriesCountJobId);
const customSettings = decodeOrThrow(jobCustomSettingsRT)(mlJob.custom_settings);
const indices = customSettings?.logs_source_config?.indexPattern;
@@ -330,7 +330,7 @@ async function fetchTopLogEntryCategories(
};
}
-async function fetchLogEntryCategories(
+export async function fetchLogEntryCategories(
context: { infra: { mlSystem: MlSystem } },
logEntryCategoriesCountJobId: string,
categoryIds: number[]
@@ -452,30 +452,6 @@ async function fetchTopLogEntryCategoryHistograms(
};
}
-async function fetchMlJob(
- context: { infra: { mlAnomalyDetectors: MlAnomalyDetectors } },
- logEntryCategoriesCountJobId: string
-) {
- const finalizeMlGetJobSpan = startTracingSpan('Fetch ml job from ES');
-
- const {
- jobs: [mlJob],
- } = await context.infra.mlAnomalyDetectors.jobs(logEntryCategoriesCountJobId);
-
- const mlGetJobSpan = finalizeMlGetJobSpan();
-
- if (mlJob == null) {
- throw new NoLogAnalysisMlJobError(`Failed to find ml job ${logEntryCategoriesCountJobId}.`);
- }
-
- return {
- mlJob,
- timing: {
- spans: [mlGetJobSpan],
- },
- };
-}
-
async function fetchLogEntryCategoryExamples(
requestContext: { core: { elasticsearch: { legacy: { client: ILegacyScopedClusterClient } } } },
indices: string,
diff --git a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_rate_analysis.ts b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_rate_analysis.ts
index 290cf03b67365..0323980dcd013 100644
--- a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_rate_analysis.ts
+++ b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_rate_analysis.ts
@@ -7,7 +7,6 @@
import { pipe } from 'fp-ts/lib/pipeable';
import { map, fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
-import { RequestHandlerContext } from 'src/core/server';
import { throwErrors, createPlainError } from '../../../common/runtime_types';
import {
logRateModelPlotResponseRT,
@@ -15,22 +14,9 @@ import {
LogRateModelPlotBucket,
CompositeTimestampPartitionKey,
} from './queries';
-import { startTracingSpan } from '../../../common/performance_tracing';
-import { decodeOrThrow } from '../../../common/runtime_types';
-import { getJobId, jobCustomSettingsRT } from '../../../common/log_analysis';
-import {
- createLogEntryRateExamplesQuery,
- logEntryRateExamplesResponseRT,
-} from './queries/log_entry_rate_examples';
-import {
- InsufficientLogAnalysisMlJobConfigurationError,
- NoLogAnalysisMlJobError,
- NoLogAnalysisResultsIndexError,
-} from './errors';
-import { InfraSource } from '../sources';
+import { getJobId } from '../../../common/log_analysis';
+import { NoLogAnalysisResultsIndexError } from './errors';
import type { MlSystem } from '../../types';
-import { InfraRequestHandlerContext } from '../../types';
-import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter';
const COMPOSITE_AGGREGATION_BATCH_SIZE = 1000;
@@ -143,130 +129,3 @@ export async function getLogEntryRateBuckets(
}
}, []);
}
-
-export async function getLogEntryRateExamples(
- context: RequestHandlerContext & { infra: Required },
- sourceId: string,
- startTime: number,
- endTime: number,
- dataset: string,
- exampleCount: number,
- sourceConfiguration: InfraSource,
- callWithRequest: KibanaFramework['callWithRequest']
-) {
- const finalizeLogEntryRateExamplesSpan = startTracingSpan(
- 'get log entry rate example log entries'
- );
-
- const jobId = getJobId(context.infra.spaceId, sourceId, 'log-entry-rate');
-
- const {
- mlJob,
- timing: { spans: fetchMlJobSpans },
- } = await fetchMlJob(context, jobId);
-
- const customSettings = decodeOrThrow(jobCustomSettingsRT)(mlJob.custom_settings);
- const indices = customSettings?.logs_source_config?.indexPattern;
- const timestampField = customSettings?.logs_source_config?.timestampField;
- const tiebreakerField = sourceConfiguration.configuration.fields.tiebreaker;
-
- if (indices == null || timestampField == null) {
- throw new InsufficientLogAnalysisMlJobConfigurationError(
- `Failed to find index configuration for ml job ${jobId}`
- );
- }
-
- const {
- examples,
- timing: { spans: fetchLogEntryRateExamplesSpans },
- } = await fetchLogEntryRateExamples(
- context,
- indices,
- timestampField,
- tiebreakerField,
- startTime,
- endTime,
- dataset,
- exampleCount,
- callWithRequest
- );
-
- const logEntryRateExamplesSpan = finalizeLogEntryRateExamplesSpan();
-
- return {
- data: examples,
- timing: {
- spans: [logEntryRateExamplesSpan, ...fetchMlJobSpans, ...fetchLogEntryRateExamplesSpans],
- },
- };
-}
-
-export async function fetchLogEntryRateExamples(
- context: RequestHandlerContext & { infra: Required },
- indices: string,
- timestampField: string,
- tiebreakerField: string,
- startTime: number,
- endTime: number,
- dataset: string,
- exampleCount: number,
- callWithRequest: KibanaFramework['callWithRequest']
-) {
- const finalizeEsSearchSpan = startTracingSpan('Fetch log rate examples from ES');
-
- const {
- hits: { hits },
- } = decodeOrThrow(logEntryRateExamplesResponseRT)(
- await callWithRequest(
- context,
- 'search',
- createLogEntryRateExamplesQuery(
- indices,
- timestampField,
- tiebreakerField,
- startTime,
- endTime,
- dataset,
- exampleCount
- )
- )
- );
-
- const esSearchSpan = finalizeEsSearchSpan();
-
- return {
- examples: hits.map((hit) => ({
- id: hit._id,
- dataset,
- message: hit._source.message ?? '',
- timestamp: hit.sort[0],
- tiebreaker: hit.sort[1],
- })),
- timing: {
- spans: [esSearchSpan],
- },
- };
-}
-
-async function fetchMlJob(
- context: RequestHandlerContext & { infra: Required },
- logEntryRateJobId: string
-) {
- const finalizeMlGetJobSpan = startTracingSpan('Fetch ml job from ES');
- const {
- jobs: [mlJob],
- } = await context.infra.mlAnomalyDetectors.jobs(logEntryRateJobId);
-
- const mlGetJobSpan = finalizeMlGetJobSpan();
-
- if (mlJob == null) {
- throw new NoLogAnalysisMlJobError(`Failed to find ml job ${logEntryRateJobId}.`);
- }
-
- return {
- mlJob,
- timing: {
- spans: [mlGetJobSpan],
- },
- };
-}
diff --git a/x-pack/plugins/infra/server/lib/log_analysis/queries/common.ts b/x-pack/plugins/infra/server/lib/log_analysis/queries/common.ts
index eacf29b303db0..87394028095de 100644
--- a/x-pack/plugins/infra/server/lib/log_analysis/queries/common.ts
+++ b/x-pack/plugins/infra/server/lib/log_analysis/queries/common.ts
@@ -21,6 +21,14 @@ export const createJobIdFilters = (jobId: string) => [
},
];
+export const createJobIdsFilters = (jobIds: string[]) => [
+ {
+ terms: {
+ job_id: jobIds,
+ },
+ },
+];
+
export const createTimeRangeFilters = (startTime: number, endTime: number) => [
{
range: {
diff --git a/x-pack/plugins/infra/server/lib/log_analysis/queries/index.ts b/x-pack/plugins/infra/server/lib/log_analysis/queries/index.ts
index 8c470acbf02fb..792c5bf98b538 100644
--- a/x-pack/plugins/infra/server/lib/log_analysis/queries/index.ts
+++ b/x-pack/plugins/infra/server/lib/log_analysis/queries/index.ts
@@ -6,3 +6,4 @@
export * from './log_entry_rate';
export * from './top_log_entry_categories';
+export * from './log_entry_anomalies';
diff --git a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_anomalies.ts b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_anomalies.ts
new file mode 100644
index 0000000000000..fc72776ea5cac
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_anomalies.ts
@@ -0,0 +1,128 @@
+/*
+ * 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 * as rt from 'io-ts';
+import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types';
+import {
+ createJobIdsFilters,
+ createTimeRangeFilters,
+ createResultTypeFilters,
+ defaultRequestParameters,
+} from './common';
+import { Sort, Pagination } from '../../../../common/http_api/log_analysis';
+
+// TODO: Reassess validity of this against ML docs
+const TIEBREAKER_FIELD = '_doc';
+
+const sortToMlFieldMap = {
+ dataset: 'partition_field_value',
+ anomalyScore: 'record_score',
+ startTime: 'timestamp',
+};
+
+export const createLogEntryAnomaliesQuery = (
+ jobIds: string[],
+ startTime: number,
+ endTime: number,
+ sort: Sort,
+ pagination: Pagination
+) => {
+ const { field } = sort;
+ const { pageSize } = pagination;
+
+ const filters = [
+ ...createJobIdsFilters(jobIds),
+ ...createTimeRangeFilters(startTime, endTime),
+ ...createResultTypeFilters(['record']),
+ ];
+
+ const sourceFields = [
+ 'job_id',
+ 'record_score',
+ 'typical',
+ 'actual',
+ 'partition_field_value',
+ 'timestamp',
+ 'bucket_span',
+ 'by_field_value',
+ ];
+
+ const { querySortDirection, queryCursor } = parsePaginationCursor(sort, pagination);
+
+ const sortOptions = [
+ { [sortToMlFieldMap[field]]: querySortDirection },
+ { [TIEBREAKER_FIELD]: querySortDirection }, // Tiebreaker
+ ];
+
+ const resultsQuery = {
+ ...defaultRequestParameters,
+ body: {
+ query: {
+ bool: {
+ filter: filters,
+ },
+ },
+ search_after: queryCursor,
+ sort: sortOptions,
+ size: pageSize,
+ _source: sourceFields,
+ },
+ };
+
+ return resultsQuery;
+};
+
+export const logEntryAnomalyHitRT = rt.type({
+ _id: rt.string,
+ _source: rt.intersection([
+ rt.type({
+ job_id: rt.string,
+ record_score: rt.number,
+ typical: rt.array(rt.number),
+ actual: rt.array(rt.number),
+ partition_field_value: rt.string,
+ bucket_span: rt.number,
+ timestamp: rt.number,
+ }),
+ rt.partial({
+ by_field_value: rt.string,
+ }),
+ ]),
+ sort: rt.tuple([rt.union([rt.string, rt.number]), rt.union([rt.string, rt.number])]),
+});
+
+export type LogEntryAnomalyHit = rt.TypeOf;
+
+export const logEntryAnomaliesResponseRT = rt.intersection([
+ commonSearchSuccessResponseFieldsRT,
+ rt.type({
+ hits: rt.type({
+ hits: rt.array(logEntryAnomalyHitRT),
+ }),
+ }),
+]);
+
+export type LogEntryAnomaliesResponseRT = rt.TypeOf;
+
+const parsePaginationCursor = (sort: Sort, pagination: Pagination) => {
+ const { cursor } = pagination;
+ const { direction } = sort;
+
+ if (!cursor) {
+ return { querySortDirection: direction, queryCursor: undefined };
+ }
+
+ // We will always use ES's search_after to paginate, to mimic "search_before" behaviour we
+ // need to reverse the user's chosen search direction for the ES query.
+ if ('searchBefore' in cursor) {
+ return {
+ querySortDirection: direction === 'desc' ? 'asc' : 'desc',
+ queryCursor: cursor.searchBefore,
+ };
+ } else {
+ return { querySortDirection: direction, queryCursor: cursor.searchAfter };
+ }
+};
diff --git a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_rate_examples.ts b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_examples.ts
similarity index 59%
rename from x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_rate_examples.ts
rename to x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_examples.ts
index ef06641caf797..74a664e78dcd6 100644
--- a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_rate_examples.ts
+++ b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_examples.ts
@@ -10,14 +10,15 @@ import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearc
import { defaultRequestParameters } from './common';
import { partitionField } from '../../../../common/log_analysis';
-export const createLogEntryRateExamplesQuery = (
+export const createLogEntryExamplesQuery = (
indices: string,
timestampField: string,
tiebreakerField: string,
startTime: number,
endTime: number,
dataset: string,
- exampleCount: number
+ exampleCount: number,
+ categoryQuery?: string
) => ({
...defaultRequestParameters,
body: {
@@ -32,11 +33,27 @@ export const createLogEntryRateExamplesQuery = (
},
},
},
- {
- term: {
- [partitionField]: dataset,
- },
- },
+ ...(!!dataset
+ ? [
+ {
+ term: {
+ [partitionField]: dataset,
+ },
+ },
+ ]
+ : []),
+ ...(categoryQuery
+ ? [
+ {
+ match: {
+ message: {
+ query: categoryQuery,
+ operator: 'AND',
+ },
+ },
+ },
+ ]
+ : []),
],
},
},
@@ -47,7 +64,7 @@ export const createLogEntryRateExamplesQuery = (
size: exampleCount,
});
-export const logEntryRateExampleHitRT = rt.type({
+export const logEntryExampleHitRT = rt.type({
_id: rt.string,
_source: rt.partial({
event: rt.partial({
@@ -58,15 +75,15 @@ export const logEntryRateExampleHitRT = rt.type({
sort: rt.tuple([rt.number, rt.number]),
});
-export type LogEntryRateExampleHit = rt.TypeOf;
+export type LogEntryExampleHit = rt.TypeOf;
-export const logEntryRateExamplesResponseRT = rt.intersection([
+export const logEntryExamplesResponseRT = rt.intersection([
commonSearchSuccessResponseFieldsRT,
rt.type({
hits: rt.type({
- hits: rt.array(logEntryRateExampleHitRT),
+ hits: rt.array(logEntryExampleHitRT),
}),
}),
]);
-export type LogEntryRateExamplesResponse = rt.TypeOf;
+export type LogEntryExamplesResponse = rt.TypeOf;
diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/index.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/index.ts
index 30b6be435837b..cbd89db97236f 100644
--- a/x-pack/plugins/infra/server/routes/log_analysis/results/index.ts
+++ b/x-pack/plugins/infra/server/routes/log_analysis/results/index.ts
@@ -8,4 +8,5 @@ export * from './log_entry_categories';
export * from './log_entry_category_datasets';
export * from './log_entry_category_examples';
export * from './log_entry_rate';
-export * from './log_entry_rate_examples';
+export * from './log_entry_examples';
+export * from './log_entry_anomalies';
diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_anomalies.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_anomalies.ts
new file mode 100644
index 0000000000000..f4911658ea496
--- /dev/null
+++ b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_anomalies.ts
@@ -0,0 +1,112 @@
+/*
+ * 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 Boom from 'boom';
+import { InfraBackendLibs } from '../../../lib/infra_types';
+import {
+ LOG_ANALYSIS_GET_LOG_ENTRY_ANOMALIES_PATH,
+ getLogEntryAnomaliesSuccessReponsePayloadRT,
+ getLogEntryAnomaliesRequestPayloadRT,
+ GetLogEntryAnomaliesRequestPayload,
+ Sort,
+ Pagination,
+} from '../../../../common/http_api/log_analysis';
+import { createValidationFunction } from '../../../../common/runtime_types';
+import { assertHasInfraMlPlugins } from '../../../utils/request_context';
+import { getLogEntryAnomalies } from '../../../lib/log_analysis';
+
+export const initGetLogEntryAnomaliesRoute = ({ framework }: InfraBackendLibs) => {
+ framework.registerRoute(
+ {
+ method: 'post',
+ path: LOG_ANALYSIS_GET_LOG_ENTRY_ANOMALIES_PATH,
+ validate: {
+ body: createValidationFunction(getLogEntryAnomaliesRequestPayloadRT),
+ },
+ },
+ framework.router.handleLegacyErrors(async (requestContext, request, response) => {
+ const {
+ data: {
+ sourceId,
+ timeRange: { startTime, endTime },
+ sort: sortParam,
+ pagination: paginationParam,
+ },
+ } = request.body;
+
+ const { sort, pagination } = getSortAndPagination(sortParam, paginationParam);
+
+ try {
+ assertHasInfraMlPlugins(requestContext);
+
+ const {
+ data: logEntryAnomalies,
+ paginationCursors,
+ hasMoreEntries,
+ timing,
+ } = await getLogEntryAnomalies(
+ requestContext,
+ sourceId,
+ startTime,
+ endTime,
+ sort,
+ pagination
+ );
+
+ return response.ok({
+ body: getLogEntryAnomaliesSuccessReponsePayloadRT.encode({
+ data: {
+ anomalies: logEntryAnomalies,
+ hasMoreEntries,
+ paginationCursors,
+ },
+ timing,
+ }),
+ });
+ } catch (error) {
+ if (Boom.isBoom(error)) {
+ throw error;
+ }
+
+ return response.customError({
+ statusCode: error.statusCode ?? 500,
+ body: {
+ message: error.message ?? 'An unexpected error occurred',
+ },
+ });
+ }
+ })
+ );
+};
+
+const getSortAndPagination = (
+ sort: Partial = {},
+ pagination: Partial = {}
+): {
+ sort: Sort;
+ pagination: Pagination;
+} => {
+ const sortDefaults = {
+ field: 'anomalyScore' as const,
+ direction: 'desc' as const,
+ };
+
+ const sortWithDefaults = {
+ ...sortDefaults,
+ ...sort,
+ };
+
+ const paginationDefaults = {
+ pageSize: 50,
+ };
+
+ const paginationWithDefaults = {
+ ...paginationDefaults,
+ ...pagination,
+ };
+
+ return { sort: sortWithDefaults, pagination: paginationWithDefaults };
+};
diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate_examples.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_examples.ts
similarity index 75%
rename from x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate_examples.ts
rename to x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_examples.ts
index b8ebcc66911dc..be4caee769506 100644
--- a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate_examples.ts
+++ b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_examples.ts
@@ -7,21 +7,21 @@
import Boom from 'boom';
import { createValidationFunction } from '../../../../common/runtime_types';
import { InfraBackendLibs } from '../../../lib/infra_types';
-import { NoLogAnalysisResultsIndexError, getLogEntryRateExamples } from '../../../lib/log_analysis';
+import { NoLogAnalysisResultsIndexError, getLogEntryExamples } from '../../../lib/log_analysis';
import { assertHasInfraMlPlugins } from '../../../utils/request_context';
import {
- getLogEntryRateExamplesRequestPayloadRT,
- getLogEntryRateExamplesSuccessReponsePayloadRT,
+ getLogEntryExamplesRequestPayloadRT,
+ getLogEntryExamplesSuccessReponsePayloadRT,
LOG_ANALYSIS_GET_LOG_ENTRY_RATE_EXAMPLES_PATH,
} from '../../../../common/http_api/log_analysis';
-export const initGetLogEntryRateExamplesRoute = ({ framework, sources }: InfraBackendLibs) => {
+export const initGetLogEntryExamplesRoute = ({ framework, sources }: InfraBackendLibs) => {
framework.registerRoute(
{
method: 'post',
path: LOG_ANALYSIS_GET_LOG_ENTRY_RATE_EXAMPLES_PATH,
validate: {
- body: createValidationFunction(getLogEntryRateExamplesRequestPayloadRT),
+ body: createValidationFunction(getLogEntryExamplesRequestPayloadRT),
},
},
framework.router.handleLegacyErrors(async (requestContext, request, response) => {
@@ -31,6 +31,7 @@ export const initGetLogEntryRateExamplesRoute = ({ framework, sources }: InfraBa
exampleCount,
sourceId,
timeRange: { startTime, endTime },
+ categoryId,
},
} = request.body;
@@ -42,7 +43,7 @@ export const initGetLogEntryRateExamplesRoute = ({ framework, sources }: InfraBa
try {
assertHasInfraMlPlugins(requestContext);
- const { data: logEntryRateExamples, timing } = await getLogEntryRateExamples(
+ const { data: logEntryExamples, timing } = await getLogEntryExamples(
requestContext,
sourceId,
startTime,
@@ -50,13 +51,14 @@ export const initGetLogEntryRateExamplesRoute = ({ framework, sources }: InfraBa
dataset,
exampleCount,
sourceConfiguration,
- framework.callWithRequest
+ framework.callWithRequest,
+ categoryId
);
return response.ok({
- body: getLogEntryRateExamplesSuccessReponsePayloadRT.encode({
+ body: getLogEntryExamplesSuccessReponsePayloadRT.encode({
data: {
- examples: logEntryRateExamples,
+ examples: logEntryExamples,
},
timing,
}),
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 4c83fa71a7060..c1f36372ec94e 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -7471,7 +7471,6 @@
"xpack.infra.logs.analysis.anomaliesSectionLineSeriesName": "15 分ごとのログエントリー (平均)",
"xpack.infra.logs.analysis.anomaliesSectionLoadingAriaLabel": "異常を読み込み中",
"xpack.infra.logs.analysis.anomaliesSectionTitle": "異常",
- "xpack.infra.logs.analysis.anomalySectionNoAnomaliesTitle": "異常が検出されませんでした。",
"xpack.infra.logs.analysis.anomalySectionNoDataBody": "時間範囲を調整する必要があるかもしれません。",
"xpack.infra.logs.analysis.anomalySectionNoDataTitle": "表示するデータがありません。",
"xpack.infra.logs.analysis.jobConfigurationOutdatedCalloutMessage": "異なるソース構成を使用して ML ジョブが作成されました。現在の構成を適用するにはジョブを再作成してください。これにより以前検出された異常が削除されます。",
@@ -7480,14 +7479,6 @@
"xpack.infra.logs.analysis.jobDefinitionOutdatedCalloutTitle": "古い ML ジョブ定義",
"xpack.infra.logs.analysis.jobStoppedCalloutMessage": "ML ジョブが手動またはリソース不足により停止しました。新しいログエントリーはジョブが再起動するまで処理されません。",
"xpack.infra.logs.analysis.jobStoppedCalloutTitle": "ML ジョブが停止しました",
- "xpack.infra.logs.analysis.logRateResultsToolbarText": "{startTime} から {endTime} までの {numberOfLogs} 件のログエントリーを分析しました",
- "xpack.infra.logs.analysis.logRateSectionBucketSpanLabel": "バケットスパン: ",
- "xpack.infra.logs.analysis.logRateSectionBucketSpanValue": "15 分",
- "xpack.infra.logs.analysis.logRateSectionLineSeriesName": "15 分ごとのログエントリー (平均)",
- "xpack.infra.logs.analysis.logRateSectionLoadingAriaLabel": "ログレートの結果を読み込み中",
- "xpack.infra.logs.analysis.logRateSectionNoDataBody": "時間範囲を調整する必要があるかもしれません。",
- "xpack.infra.logs.analysis.logRateSectionNoDataTitle": "表示するデータがありません。",
- "xpack.infra.logs.analysis.logRateSectionTitle": "ログレート",
"xpack.infra.logs.analysis.missingMlResultsPrivilegesBody": "本機能は機械学習ジョブを利用し、そのステータスと結果にアクセスするためには、少なくとも{machineLearningUserRole}ロールが必要です。",
"xpack.infra.logs.analysis.missingMlResultsPrivilegesTitle": "追加の機械学習の権限が必要です",
"xpack.infra.logs.analysis.missingMlSetupPrivilegesBody": "本機能は機械学習ジョブを利用し、設定には{machineLearningAdminRole}ロールが必要です。",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 86b2480e3b314..7e36d5676585c 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -7476,7 +7476,6 @@
"xpack.infra.logs.analysis.anomaliesSectionLineSeriesName": "每 15 分钟日志条目数(平均值)",
"xpack.infra.logs.analysis.anomaliesSectionLoadingAriaLabel": "正在加载异常",
"xpack.infra.logs.analysis.anomaliesSectionTitle": "异常",
- "xpack.infra.logs.analysis.anomalySectionNoAnomaliesTitle": "未检测到任何异常。",
"xpack.infra.logs.analysis.anomalySectionNoDataBody": "您可能想调整时间范围。",
"xpack.infra.logs.analysis.anomalySectionNoDataTitle": "没有可显示的数据。",
"xpack.infra.logs.analysis.jobConfigurationOutdatedCalloutMessage": "创建 ML 作业时所使用的源配置不同。重新创建作业以应用当前配置。这将移除以前检测到的异常。",
@@ -7485,14 +7484,6 @@
"xpack.infra.logs.analysis.jobDefinitionOutdatedCalloutTitle": "ML 作业定义已过期",
"xpack.infra.logs.analysis.jobStoppedCalloutMessage": "ML 作业已手动停止或由于缺乏资源而停止。作业重新启动后,才会处理新的日志条目。",
"xpack.infra.logs.analysis.jobStoppedCalloutTitle": "ML 作业已停止",
- "xpack.infra.logs.analysis.logRateResultsToolbarText": "从 {startTime} 到 {endTime} 已分析 {numberOfLogs} 个日志条目",
- "xpack.infra.logs.analysis.logRateSectionBucketSpanLabel": "存储桶跨度: ",
- "xpack.infra.logs.analysis.logRateSectionBucketSpanValue": "15 分钟",
- "xpack.infra.logs.analysis.logRateSectionLineSeriesName": "每 15 分钟日志条目数(平均值)",
- "xpack.infra.logs.analysis.logRateSectionLoadingAriaLabel": "正在加载日志速率结果",
- "xpack.infra.logs.analysis.logRateSectionNoDataBody": "您可能想调整时间范围。",
- "xpack.infra.logs.analysis.logRateSectionNoDataTitle": "没有可显示的数据。",
- "xpack.infra.logs.analysis.logRateSectionTitle": "日志速率",
"xpack.infra.logs.analysis.missingMlResultsPrivilegesBody": "此功能使用 Machine Learning 作业,要访问这些作业的状态和结果,至少需要 {machineLearningUserRole} 角色。",
"xpack.infra.logs.analysis.missingMlResultsPrivilegesTitle": "需要额外的 Machine Learning 权限",
"xpack.infra.logs.analysis.missingMlSetupPrivilegesBody": "此功能使用 Machine Learning 作业,这需要 {machineLearningAdminRole} 角色才能设置。",
From 6eeff6bfb4d7e458384d04af239272071b87ab53 Mon Sep 17 00:00:00 2001
From: Brent Kimmel
Date: Mon, 13 Jul 2020 13:36:24 -0400
Subject: [PATCH 24/40] [Security_Solution][GTV] Add lineage limit warnings to
graph (#70097)
* [Security Solution][GTV] Add lineage limit warnings to graph
Co-authored-by: Elastic Machine
Co-authored-by: oatkiller
---
.../common/endpoint/models/event.ts | 15 +-
.../public/resolver/models/resolver_tree.ts | 5 +-
.../resolver/store/data/reducer.test.ts | 281 +++++++++++++++++-
.../public/resolver/store/data/selectors.ts | 114 ++++++-
.../public/resolver/store/selectors.ts | 20 ++
.../public/resolver/view/limit_warnings.tsx | 126 ++++++++
.../panels/panel_content_process_list.tsx | 27 ++
.../panels/panel_content_related_list.tsx | 48 ++-
8 files changed, 617 insertions(+), 19 deletions(-)
create mode 100644 x-pack/plugins/security_solution/public/resolver/view/limit_warnings.tsx
diff --git a/x-pack/plugins/security_solution/common/endpoint/models/event.ts b/x-pack/plugins/security_solution/common/endpoint/models/event.ts
index 86cccff957211..9b4550f52ff22 100644
--- a/x-pack/plugins/security_solution/common/endpoint/models/event.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/models/event.ts
@@ -82,7 +82,6 @@ export function getAncestryAsArray(event: ResolverEvent | undefined): string[] {
* @param event The event to get the category for
*/
export function primaryEventCategory(event: ResolverEvent): string | undefined {
- // Returning "Process" as a catch-all here because it seems pretty general
if (isLegacyEvent(event)) {
const legacyFullType = event.endgame.event_type_full;
if (legacyFullType) {
@@ -96,6 +95,20 @@ export function primaryEventCategory(event: ResolverEvent): string | undefined {
}
}
+/**
+ * @param event The event to get the full ECS category for
+ */
+export function allEventCategories(event: ResolverEvent): string | string[] | undefined {
+ if (isLegacyEvent(event)) {
+ const legacyFullType = event.endgame.event_type_full;
+ if (legacyFullType) {
+ return legacyFullType;
+ }
+ } else {
+ return event.event.category;
+ }
+}
+
/**
* ECS event type will be things like 'creation', 'deletion', 'access', etc.
* see: https://www.elastic.co/guide/en/ecs/current/ecs-event.html
diff --git a/x-pack/plugins/security_solution/public/resolver/models/resolver_tree.ts b/x-pack/plugins/security_solution/public/resolver/models/resolver_tree.ts
index cf32988a856b2..446e371832d38 100644
--- a/x-pack/plugins/security_solution/public/resolver/models/resolver_tree.ts
+++ b/x-pack/plugins/security_solution/public/resolver/models/resolver_tree.ts
@@ -9,6 +9,7 @@ import {
ResolverEvent,
ResolverNodeStats,
ResolverLifecycleNode,
+ ResolverChildNode,
} from '../../../common/endpoint/types';
import { uniquePidForProcess } from './process_event';
@@ -60,11 +61,13 @@ export function relatedEventsStats(tree: ResolverTree): Map {
let store: Store;
+ let dispatchTree: (tree: ResolverTree) => void;
beforeEach(() => {
store = createStore(dataReducer, undefined);
+ dispatchTree = (tree) => {
+ const action: DataAction = {
+ type: 'serverReturnedResolverData',
+ payload: {
+ result: tree,
+ databaseDocumentID: '',
+ },
+ };
+ store.dispatch(action);
+ };
});
describe('when data was received and the ancestry and children edges had cursors', () => {
beforeEach(() => {
- const generator = new EndpointDocGenerator('seed');
+ // Generate a 'tree' using the Resolver generator code. This structure isn't the same as what the API returns.
+ const baseTree = generateBaseTree();
const tree = mockResolverTree({
- events: generator.generateTree({ ancestors: 1, generations: 2, children: 2 }).allEvents,
+ events: baseTree.allEvents,
cursors: {
- childrenNextChild: 'aValidChildursor',
+ childrenNextChild: 'aValidChildCursor',
ancestryNextAncestor: 'aValidAncestorCursor',
},
- });
- if (tree) {
- const action: DataAction = {
- type: 'serverReturnedResolverData',
- payload: {
- result: tree,
- databaseDocumentID: '',
- },
- };
- store.dispatch(action);
- }
+ })!;
+ dispatchTree(tree);
});
it('should indicate there are additional ancestor', () => {
expect(selectors.hasMoreAncestors(store.getState())).toBe(true);
@@ -49,4 +53,251 @@ describe('Resolver Data Middleware', () => {
expect(selectors.hasMoreChildren(store.getState())).toBe(true);
});
});
+
+ describe('when data was received with stats mocked for the first child node', () => {
+ let firstChildNodeInTree: TreeNode;
+ let eventStatsForFirstChildNode: { total: number; byCategory: Record };
+ let categoryToOverCount: string;
+ let tree: ResolverTree;
+
+ /**
+ * Compiling stats to use for checking limit warnings and counts of missing events
+ * e.g. Limit warnings should show when number of related events actually displayed
+ * is lower than the estimated count from stats.
+ */
+
+ beforeEach(() => {
+ ({
+ tree,
+ firstChildNodeInTree,
+ eventStatsForFirstChildNode,
+ categoryToOverCount,
+ } = mockedTree());
+ if (tree) {
+ dispatchTree(tree);
+ }
+ });
+
+ describe('and when related events were returned with totals equalling what stat counts indicate they should be', () => {
+ beforeEach(() => {
+ // Return related events for the first child node
+ const relatedAction: DataAction = {
+ type: 'serverReturnedRelatedEventData',
+ payload: {
+ entityID: firstChildNodeInTree.id,
+ events: firstChildNodeInTree.relatedEvents,
+ nextEvent: null,
+ },
+ };
+ store.dispatch(relatedAction);
+ });
+ it('should have the correct related events', () => {
+ const selectedEventsByEntityId = selectors.relatedEventsByEntityId(store.getState());
+ const selectedEventsForFirstChildNode = selectedEventsByEntityId.get(
+ firstChildNodeInTree.id
+ )!.events;
+
+ expect(selectedEventsForFirstChildNode).toBe(firstChildNodeInTree.relatedEvents);
+ });
+ it('should indicate the correct related event count for each category', () => {
+ const selectedRelatedInfo = selectors.relatedEventInfoByEntityId(store.getState());
+ const displayCountsForCategory = selectedRelatedInfo(firstChildNodeInTree.id)
+ ?.numberActuallyDisplayedForCategory!;
+
+ const eventCategoriesForNode: string[] = Object.keys(
+ eventStatsForFirstChildNode.byCategory
+ );
+
+ for (const eventCategory of eventCategoriesForNode) {
+ expect(`${eventCategory}:${displayCountsForCategory(eventCategory)}`).toBe(
+ `${eventCategory}:${eventStatsForFirstChildNode.byCategory[eventCategory]}`
+ );
+ }
+ });
+ /**
+ * The general approach reflected here is to _avoid_ showing a limit warning - even if we hit
+ * the overall related event limit - as long as the number in our category matches what the stats
+ * say we have. E.g. If the stats say you have 20 dns events, and we receive 20 dns events, we
+ * don't need to display a limit warning for that, even if we hit some overall event limit of e.g. 100
+ * while we were fetching the 20.
+ */
+ it('should not indicate the limit has been exceeded because the number of related events received for the category is greater or equal to the stats count', () => {
+ const selectedRelatedInfo = selectors.relatedEventInfoByEntityId(store.getState());
+ const shouldShowLimit = selectedRelatedInfo(firstChildNodeInTree.id)
+ ?.shouldShowLimitForCategory!;
+ for (const typeCounted of Object.keys(eventStatsForFirstChildNode.byCategory)) {
+ expect(shouldShowLimit(typeCounted)).toBe(false);
+ }
+ });
+ it('should not indicate that there are any related events missing because the number of related events received for the category is greater or equal to the stats count', () => {
+ const selectedRelatedInfo = selectors.relatedEventInfoByEntityId(store.getState());
+ const notDisplayed = selectedRelatedInfo(firstChildNodeInTree.id)
+ ?.numberNotDisplayedForCategory!;
+ for (const typeCounted of Object.keys(eventStatsForFirstChildNode.byCategory)) {
+ expect(notDisplayed(typeCounted)).toBe(0);
+ }
+ });
+ });
+ describe('when data was received and stats show more related events than the API can provide', () => {
+ beforeEach(() => {
+ // Add 1 to the stats for an event category so that the selectors think we are missing data.
+ // This mutates `tree`, and then we re-dispatch it
+ eventStatsForFirstChildNode.byCategory[categoryToOverCount] =
+ eventStatsForFirstChildNode.byCategory[categoryToOverCount] + 1;
+
+ if (tree) {
+ dispatchTree(tree);
+ const relatedAction: DataAction = {
+ type: 'serverReturnedRelatedEventData',
+ payload: {
+ entityID: firstChildNodeInTree.id,
+ events: firstChildNodeInTree.relatedEvents,
+ nextEvent: 'aValidNextEventCursor',
+ },
+ };
+ store.dispatch(relatedAction);
+ }
+ });
+ it('should have the correct related events', () => {
+ const selectedEventsByEntityId = selectors.relatedEventsByEntityId(store.getState());
+ const selectedEventsForFirstChildNode = selectedEventsByEntityId.get(
+ firstChildNodeInTree.id
+ )!.events;
+
+ expect(selectedEventsForFirstChildNode).toBe(firstChildNodeInTree.relatedEvents);
+ });
+ it('should indicate the limit has been exceeded because the number of related events received for the category is less than what the stats count said it would be', () => {
+ const selectedRelatedInfo = selectors.relatedEventInfoByEntityId(store.getState());
+ const shouldShowLimit = selectedRelatedInfo(firstChildNodeInTree.id)
+ ?.shouldShowLimitForCategory!;
+ expect(shouldShowLimit(categoryToOverCount)).toBe(true);
+ });
+ it('should indicate that there are related events missing because the number of related events received for the category is less than what the stats count said it would be', () => {
+ const selectedRelatedInfo = selectors.relatedEventInfoByEntityId(store.getState());
+ const notDisplayed = selectedRelatedInfo(firstChildNodeInTree.id)
+ ?.numberNotDisplayedForCategory!;
+ expect(notDisplayed(categoryToOverCount)).toBe(1);
+ });
+ });
+ });
});
+
+function mockedTree() {
+ // Generate a 'tree' using the Resolver generator code. This structure isn't the same as what the API returns.
+ const baseTree = generateBaseTree();
+
+ const { children } = baseTree;
+ const firstChildNodeInTree = [...children.values()][0];
+
+ // The `generateBaseTree` mock doesn't calculate stats (the actual data has them.)
+ // So calculate some stats for just the node that we'll test.
+ const statsResults = compileStatsForChild(firstChildNodeInTree);
+
+ const tree = mockResolverTree({
+ events: baseTree.allEvents,
+ /**
+ * Calculate children from the ResolverTree response using the children of the `Tree` we generated using the Resolver data generator code.
+ * Compile (and attach) stats to the first child node.
+ *
+ * The purpose of `children` here is to set the `actual`
+ * value that the stats values will be compared with
+ * to derive things like the number of missing events and if
+ * related event limits should be shown.
+ */
+ children: [...baseTree.children.values()].map((node: TreeNode) => {
+ // Treat each `TreeNode` as a `ResolverChildNode`.
+ // These types are almost close enough to be used interchangably (for the purposes of this test.)
+ const childNode: Partial = node;
+
+ // `TreeNode` has `id` which is the same as `entityID`.
+ // The `ResolverChildNode` calls the entityID as `entityID`.
+ // Set `entityID` on `childNode` since the code in test relies on it.
+ childNode.entityID = (childNode as TreeNode).id;
+
+ // This should only be true for the first child.
+ if (node.id === firstChildNodeInTree.id) {
+ // attach stats
+ childNode.stats = {
+ events: statsResults.eventStats,
+ totalAlerts: 0,
+ };
+ }
+ return childNode;
+ }) as ResolverChildNode[] /**
+ Cast to ResolverChildNode[] array is needed because incoming
+ TreeNodes from the generator cannot be assigned cleanly to the
+ tree model's expected ResolverChildNode type.
+ */,
+ });
+
+ return {
+ tree: tree!,
+ firstChildNodeInTree,
+ eventStatsForFirstChildNode: statsResults.eventStats,
+ categoryToOverCount: statsResults.firstCategory,
+ };
+}
+
+function generateBaseTree() {
+ const generator = new EndpointDocGenerator('seed');
+ return generator.generateTree({
+ ancestors: 1,
+ generations: 2,
+ children: 3,
+ percentWithRelated: 100,
+ alwaysGenMaxChildrenPerNode: true,
+ });
+}
+
+function compileStatsForChild(
+ node: TreeNode
+): {
+ eventStats: {
+ /** The total number of related events. */
+ total: number;
+ /** A record with the categories of events as keys, and the count of events per category as values. */
+ byCategory: Record;
+ };
+ /** The category of the first event. */
+ firstCategory: string;
+} {
+ const totalRelatedEvents = node.relatedEvents.length;
+ // For the purposes of testing, we pick one category to fake an extra event for
+ // so we can test if the event limit selectors do the right thing.
+
+ let firstCategory: string | undefined;
+
+ const compiledStats = node.relatedEvents.reduce(
+ (counts: Record, relatedEvent) => {
+ // `relatedEvent.event.category` is `string | string[]`.
+ // Wrap it in an array and flatten that array to get a `string[] | [string]`
+ // which we can loop over.
+ const categories: string[] = [relatedEvent.event.category].flat();
+
+ for (const category of categories) {
+ // Set the first category as 'categoryToOverCount'
+ if (firstCategory === undefined) {
+ firstCategory = category;
+ }
+
+ // Increment the count of events with this category
+ counts[category] = counts[category] ? counts[category] + 1 : 1;
+ }
+ return counts;
+ },
+ {}
+ );
+ if (firstCategory === undefined) {
+ throw new Error('there were no related events for the node.');
+ }
+ return {
+ /**
+ * Object to use for the first child nodes stats `events` object?
+ */
+ eventStats: {
+ total: totalRelatedEvents,
+ byCategory: compiledStats,
+ },
+ firstCategory,
+ };
+}
diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts
index 9c47c765457e3..990b911e5dbd0 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts
@@ -5,7 +5,7 @@
*/
import rbush from 'rbush';
-import { createSelector } from 'reselect';
+import { createSelector, defaultMemoize } from 'reselect';
import {
DataState,
AdjacentProcessMap,
@@ -32,6 +32,7 @@ import {
} from '../../../../common/endpoint/types';
import * as resolverTreeModel from '../../models/resolver_tree';
import { isometricTaxiLayout } from '../../models/indexed_process_tree/isometric_taxi_layout';
+import { allEventCategories } from '../../../../common/endpoint/models/event';
/**
* If there is currently a request.
@@ -167,6 +168,116 @@ export function hasMoreAncestors(state: DataState): boolean {
return tree ? resolverTreeModel.hasMoreAncestors(tree) : false;
}
+interface RelatedInfoFunctions {
+ shouldShowLimitForCategory: (category: string) => boolean;
+ numberNotDisplayedForCategory: (category: string) => number;
+ numberActuallyDisplayedForCategory: (category: string) => number;
+}
+/**
+ * A map of `entity_id`s to functions that provide information about
+ * related events by ECS `.category` Primarily to avoid having business logic
+ * in UI components.
+ */
+export const relatedEventInfoByEntityId: (
+ state: DataState
+) => (entityID: string) => RelatedInfoFunctions | null = createSelector(
+ relatedEventsByEntityId,
+ relatedEventsStats,
+ function selectLineageLimitInfo(
+ /* eslint-disable no-shadow */
+ relatedEventsByEntityId,
+ relatedEventsStats
+ /* eslint-enable no-shadow */
+ ) {
+ if (!relatedEventsStats) {
+ // If there are no related event stats, there are no related event info objects
+ return (entityId: string) => null;
+ }
+ return (entityId) => {
+ const stats = relatedEventsStats.get(entityId);
+ if (!stats) {
+ return null;
+ }
+ const eventsResponseForThisEntry = relatedEventsByEntityId.get(entityId);
+ const hasMoreEvents =
+ eventsResponseForThisEntry && eventsResponseForThisEntry.nextEvent !== null;
+ /**
+ * Get the "aggregate" total for the event category (i.e. _all_ events that would qualify as being "in category")
+ * For a set like `[DNS,File][File,DNS][Registry]` The first and second events would contribute to the aggregate total for DNS being 2.
+ * This is currently aligned with how the backed provides this information.
+ *
+ * @param eventCategory {string} The ECS category like 'file','dns',etc.
+ */
+ const aggregateTotalForCategory = (eventCategory: string): number => {
+ return stats.events.byCategory[eventCategory] || 0;
+ };
+
+ /**
+ * Get all the related events in the category provided.
+ *
+ * @param eventCategory {string} The ECS category like 'file','dns',etc.
+ */
+ const unmemoizedMatchingEventsForCategory = (eventCategory: string): ResolverEvent[] => {
+ if (!eventsResponseForThisEntry) {
+ return [];
+ }
+ return eventsResponseForThisEntry.events.filter((resolverEvent) => {
+ for (const category of [allEventCategories(resolverEvent)].flat()) {
+ if (category === eventCategory) {
+ return true;
+ }
+ }
+ return false;
+ });
+ };
+
+ const matchingEventsForCategory = defaultMemoize(unmemoizedMatchingEventsForCategory);
+
+ /**
+ * The number of events that occurred before the API limit was reached.
+ * The number of events that came back form the API that have `eventCategory` in their list of categories.
+ *
+ * @param eventCategory {string} The ECS category like 'file','dns',etc.
+ */
+ const numberActuallyDisplayedForCategory = (eventCategory: string): number => {
+ return matchingEventsForCategory(eventCategory)?.length || 0;
+ };
+
+ /**
+ * The total number counted by the backend - the number displayed
+ *
+ * @param eventCategory {string} The ECS category like 'file','dns',etc.
+ */
+ const numberNotDisplayedForCategory = (eventCategory: string): number => {
+ return (
+ aggregateTotalForCategory(eventCategory) -
+ numberActuallyDisplayedForCategory(eventCategory)
+ );
+ };
+
+ /**
+ * `true` when the `nextEvent` cursor appeared in the results and we are short on the number needed to
+ * fullfill the aggregate count.
+ *
+ * @param eventCategory {string} The ECS category like 'file','dns',etc.
+ */
+ const shouldShowLimitForCategory = (eventCategory: string): boolean => {
+ if (hasMoreEvents && numberNotDisplayedForCategory(eventCategory) > 0) {
+ return true;
+ }
+ return false;
+ };
+
+ const entryValue = {
+ shouldShowLimitForCategory,
+ numberNotDisplayedForCategory,
+ numberActuallyDisplayedForCategory,
+ };
+ return entryValue;
+ };
+ }
+);
+
/**
* If we need to fetch, this is the ID to fetch.
*/
@@ -285,6 +396,7 @@ export const visibleProcessNodePositionsAndEdgeLineSegments = createSelector(
};
}
);
+
/**
* If there is a pending request that's for a entity ID that doesn't matche the `entityID`, then we should cancel it.
*/
diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts
index 2bc254d118d33..6e512cfe13f62 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts
@@ -103,6 +103,16 @@ export const relatedEventsReady = composeSelectors(
dataSelectors.relatedEventsReady
);
+/**
+ * Business logic lookup functions by ECS category by entity id.
+ * Example usage:
+ * const numberOfFileEvents = infoByEntityId.get(`someEntityId`)?.getAggregateTotalForCategory(`file`);
+ */
+export const relatedEventInfoByEntityId = composeSelectors(
+ dataStateSelector,
+ dataSelectors.relatedEventInfoByEntityId
+);
+
/**
* Returns the id of the "current" tree node (fake-focused)
*/
@@ -158,6 +168,16 @@ export const isLoading = composeSelectors(dataStateSelector, dataSelectors.isLoa
*/
export const hasError = composeSelectors(dataStateSelector, dataSelectors.hasError);
+/**
+ * True if the children cursor is not null
+ */
+export const hasMoreChildren = composeSelectors(dataStateSelector, dataSelectors.hasMoreChildren);
+
+/**
+ * True if the ancestor cursor is not null
+ */
+export const hasMoreAncestors = composeSelectors(dataStateSelector, dataSelectors.hasMoreAncestors);
+
/**
* An array containing all the processes currently in the Resolver than can be graphed
*/
diff --git a/x-pack/plugins/security_solution/public/resolver/view/limit_warnings.tsx b/x-pack/plugins/security_solution/public/resolver/view/limit_warnings.tsx
new file mode 100644
index 0000000000000..e3bad8ee2e574
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/resolver/view/limit_warnings.tsx
@@ -0,0 +1,126 @@
+/*
+ * 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 { EuiCallOut } from '@elastic/eui';
+import { FormattedMessage } from 'react-intl';
+
+const lineageLimitMessage = (
+ <>
+
+ >
+);
+
+const LineageTitleMessage = React.memo(function LineageTitleMessage({
+ numberOfEntries,
+}: {
+ numberOfEntries: number;
+}) {
+ return (
+ <>
+
+ >
+ );
+});
+
+const RelatedEventsLimitMessage = React.memo(function RelatedEventsLimitMessage({
+ category,
+ numberOfEventsMissing,
+}: {
+ numberOfEventsMissing: number;
+ category: string;
+}) {
+ return (
+ <>
+
+ >
+ );
+});
+
+const RelatedLimitTitleMessage = React.memo(function RelatedLimitTitleMessage({
+ category,
+ numberOfEventsDisplayed,
+}: {
+ numberOfEventsDisplayed: number;
+ category: string;
+}) {
+ return (
+ <>
+
+ >
+ );
+});
+
+/**
+ * Limit warning for hitting the /events API limit
+ */
+export const RelatedEventLimitWarning = React.memo(function RelatedEventLimitWarning({
+ className,
+ eventType,
+ numberActuallyDisplayed,
+ numberMissing,
+}: {
+ className?: string;
+ eventType: string;
+ numberActuallyDisplayed: number;
+ numberMissing: number;
+}) {
+ /**
+ * Based on API limits, all related events may not be displayed.
+ */
+ return (
+
+ }
+ >
+
+
+
+
+ );
+});
+
+/**
+ * Limit warning for hitting a limit of nodes in the tree
+ */
+export const LimitWarning = React.memo(function LimitWarning({
+ className,
+ numberDisplayed,
+}: {
+ className?: string;
+ numberDisplayed: number;
+}) {
+ return (
+ }
+ >
+ {lineageLimitMessage}
+
+ );
+});
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx
index 9152649c07abf..0ed677885775f 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx
@@ -13,6 +13,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useSelector } from 'react-redux';
+import styled from 'styled-components';
import * as event from '../../../../common/endpoint/models/event';
import * as selectors from '../../store/selectors';
import { CrumbInfo, formatter, StyledBreadcrumbs } from './panel_content_utilities';
@@ -20,6 +21,27 @@ import { useResolverDispatch } from '../use_resolver_dispatch';
import { SideEffectContext } from '../side_effect_context';
import { CubeForProcess } from './process_cube_icon';
import { ResolverEvent } from '../../../../common/endpoint/types';
+import { LimitWarning } from '../limit_warnings';
+
+const StyledLimitWarning = styled(LimitWarning)`
+ flex-flow: row wrap;
+ display: block;
+ align-items: baseline;
+ margin-top: 1em;
+
+ & .euiCallOutHeader {
+ display: inline;
+ margin-right: 0.25em;
+ }
+
+ & .euiText {
+ display: inline;
+ }
+
+ & .euiText p {
+ display: inline;
+ }
+`;
/**
* The "default" view for the panel: A list of all the processes currently in the graph.
@@ -145,6 +167,7 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({
}),
[processNodePositions]
);
+ const numberOfProcesses = processTableView.length;
const crumbs = useMemo(() => {
return [
@@ -160,9 +183,13 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({
];
}, []);
+ const children = useSelector(selectors.hasMoreChildren);
+ const ancestors = useSelector(selectors.hasMoreAncestors);
+ const showWarning = children === true || ancestors === true;
return (
<>
+ {showWarning && }
items={processTableView} columns={columns} sorting />
>
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_list.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_list.tsx
index 1c17cf7e6ce34..591432e1f9f9f 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_list.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_list.tsx
@@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n';
import { EuiTitle, EuiSpacer, EuiText, EuiButtonEmpty, EuiHorizontalRule } from '@elastic/eui';
import { useSelector } from 'react-redux';
import { FormattedMessage } from 'react-intl';
+import styled from 'styled-components';
import {
CrumbInfo,
formatDate,
@@ -20,6 +21,7 @@ import * as event from '../../../../common/endpoint/models/event';
import { ResolverEvent, ResolverNodeStats } from '../../../../common/endpoint/types';
import * as selectors from '../../store/selectors';
import { useResolverDispatch } from '../use_resolver_dispatch';
+import { RelatedEventLimitWarning } from '../limit_warnings';
/**
* This view presents a list of related events of a given type for a given process.
@@ -40,16 +42,53 @@ interface MatchingEventEntry {
setQueryParams: () => void;
}
+const StyledRelatedLimitWarning = styled(RelatedEventLimitWarning)`
+ flex-flow: row wrap;
+ display: block;
+ align-items: baseline;
+ margin-top: 1em;
+
+ & .euiCallOutHeader {
+ display: inline;
+ margin-right: 0.25em;
+ }
+
+ & .euiText {
+ display: inline;
+ }
+
+ & .euiText p {
+ display: inline;
+ }
+`;
+
const DisplayList = memo(function DisplayList({
crumbs,
matchingEventEntries,
+ eventType,
+ processEntityId,
}: {
crumbs: Array<{ text: string | JSX.Element; onClick: () => void }>;
matchingEventEntries: MatchingEventEntry[];
+ eventType: string;
+ processEntityId: string;
}) {
+ const relatedLookupsByCategory = useSelector(selectors.relatedEventInfoByEntityId);
+ const lookupsForThisNode = relatedLookupsByCategory(processEntityId);
+ const shouldShowLimitWarning = lookupsForThisNode?.shouldShowLimitForCategory(eventType);
+ const numberDisplayed = lookupsForThisNode?.numberActuallyDisplayedForCategory(eventType);
+ const numberMissing = lookupsForThisNode?.numberNotDisplayedForCategory(eventType);
+
return (
<>
+ {shouldShowLimitWarning && typeof numberDisplayed !== 'undefined' && numberMissing ? (
+
+ ) : null}
<>
{matchingEventEntries.map((eventView, index) => {
@@ -250,6 +289,13 @@ export const ProcessEventListNarrowedByType = memo(function ProcessEventListNarr
);
}
- return ;
+ return (
+
+ );
});
ProcessEventListNarrowedByType.displayName = 'ProcessEventListNarrowedByType';
From c82ccfedc6852179e8404c1100adc13ecef6ae6f Mon Sep 17 00:00:00 2001
From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com>
Date: Mon, 13 Jul 2020 13:41:49 -0400
Subject: [PATCH 25/40] [SECURITY_SOLUTION][ENDPOINT] Sync up i18n of Policy
Response action names to the latest from Endpoint (#71472)
* Added updated Policy Response action names to translation file
* `formatResponse` to generate a user friendly value for action name if no i18n
* test case to cover formatting unknown actions
---
.../details/policy_response_friendly_names.ts | 352 +++++++++++-------
.../pages/endpoint_hosts/view/index.test.tsx | 17 +-
2 files changed, 228 insertions(+), 141 deletions(-)
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response_friendly_names.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response_friendly_names.ts
index 28e91331b428d..020e8c9e38ad5 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response_friendly_names.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response_friendly_names.ts
@@ -6,7 +6,209 @@
import { i18n } from '@kbn/i18n';
-const responseMap = new Map();
+const policyResponses: Array<[string, string]> = [
+ [
+ 'configure_dns_events',
+ i18n.translate(
+ 'xpack.securitySolution.endpoint.hostDetails.policyResponse.configure_dns_events',
+ { defaultMessage: 'Configure DNS Events' }
+ ),
+ ],
+ [
+ 'configure_elasticsearch_connection',
+ i18n.translate(
+ 'xpack.securitySolution.endpoint.hostDetails.policyResponse.configure_elasticsearch_connection',
+ { defaultMessage: 'Configure Elastic Search Connection' }
+ ),
+ ],
+ [
+ 'configure_file_events',
+ i18n.translate(
+ 'xpack.securitySolution.endpoint.hostDetails.policyResponse.configure_file_events',
+ { defaultMessage: 'Configure File Events' }
+ ),
+ ],
+ [
+ 'configure_imageload_events',
+ i18n.translate(
+ 'xpack.securitySolution.endpoint.hostDetails.policyResponse.configure_imageload_events',
+ { defaultMessage: 'Configure Image Load Events' }
+ ),
+ ],
+ [
+ 'configure_kernel',
+ i18n.translate('xpack.securitySolution.endpoint.hostDetails.policyResponse.configure_kernel', {
+ defaultMessage: 'Configure Kernel',
+ }),
+ ],
+ [
+ 'configure_logging',
+ i18n.translate('xpack.securitySolution.endpoint.hostDetails.policyResponse.configure_logging', {
+ defaultMessage: 'Configure Logging',
+ }),
+ ],
+ [
+ 'configure_malware',
+ i18n.translate('xpack.securitySolution.endpoint.hostDetails.policyResponse.configure_malware', {
+ defaultMessage: 'Configure Malware',
+ }),
+ ],
+ [
+ 'configure_network_events',
+ i18n.translate(
+ 'xpack.securitySolution.endpoint.hostDetails.policyResponse.configure_network_events',
+ { defaultMessage: 'Configure Network Events' }
+ ),
+ ],
+ [
+ 'configure_process_events',
+ i18n.translate(
+ 'xpack.securitySolution.endpoint.hostDetails.policyResponse.configure_process_events',
+ { defaultMessage: 'Configure Process Events' }
+ ),
+ ],
+ [
+ 'configure_registry_events',
+ i18n.translate(
+ 'xpack.securitySolution.endpoint.hostDetails.policyResponse.configure_registry_events',
+ { defaultMessage: 'Configure Registry Events' }
+ ),
+ ],
+ [
+ 'configure_security_events',
+ i18n.translate(
+ 'xpack.securitySolution.endpoint.hostDetails.policyResponse.configure_security_events',
+ { defaultMessage: 'Configure Security Events' }
+ ),
+ ],
+ [
+ 'connect_kernel',
+ i18n.translate('xpack.securitySolution.endpoint.hostDetails.policyResponse.connect_kernel', {
+ defaultMessage: 'Connect Kernel',
+ }),
+ ],
+ [
+ 'detect_async_image_load_events',
+ i18n.translate(
+ 'xpack.securitySolution.endpoint.hostDetails.policyResponse.detect_async_image_load_events',
+ { defaultMessage: 'Detect Async Image Load Events' }
+ ),
+ ],
+ [
+ 'detect_file_open_events',
+ i18n.translate(
+ 'xpack.securitySolution.endpoint.hostDetails.policyResponse.detect_file_open_events',
+ { defaultMessage: 'Detect File Open Events' }
+ ),
+ ],
+ [
+ 'detect_file_write_events',
+ i18n.translate(
+ 'xpack.securitySolution.endpoint.hostDetails.policyResponse.detect_file_write_events',
+ { defaultMessage: 'Detect File Write Events' }
+ ),
+ ],
+ [
+ 'detect_network_events',
+ i18n.translate(
+ 'xpack.securitySolution.endpoint.hostDetails.policyResponse.detect_network_events',
+ { defaultMessage: 'Detect Network Events' }
+ ),
+ ],
+ [
+ 'detect_process_events',
+ i18n.translate(
+ 'xpack.securitySolution.endpoint.hostDetails.policyResponse.detect_process_events',
+ { defaultMessage: 'Detect Process Events' }
+ ),
+ ],
+ [
+ 'detect_registry_events',
+ i18n.translate(
+ 'xpack.securitySolution.endpoint.hostDetails.policyResponse.detect_registry_events',
+ { defaultMessage: 'Detect Registry Events' }
+ ),
+ ],
+ [
+ 'detect_sync_image_load_events',
+ i18n.translate(
+ 'xpack.securitySolution.endpoint.hostDetails.policyResponse.detect_sync_image_load_events',
+ { defaultMessage: 'Detect Sync Image Load Events' }
+ ),
+ ],
+ [
+ 'download_global_artifacts',
+ i18n.translate(
+ 'xpack.securitySolution.endpoint.hostDetails.policyResponse.download_global_artifacts',
+ { defaultMessage: 'Download Global Artifacts' }
+ ),
+ ],
+ [
+ 'download_user_artifacts',
+ i18n.translate(
+ 'xpack.securitySolution.endpoint.hostDetails.policyResponse.download_user_artifacts',
+ { defaultMessage: 'Download User Artifacts' }
+ ),
+ ],
+ [
+ 'load_config',
+ i18n.translate('xpack.securitySolution.endpoint.hostDetails.policyResponse.load_config', {
+ defaultMessage: 'Load Config',
+ }),
+ ],
+ [
+ 'load_malware_model',
+ i18n.translate(
+ 'xpack.securitySolution.endpoint.hostDetails.policyResponse.load_malware_model',
+ { defaultMessage: 'Load Malware Model' }
+ ),
+ ],
+ [
+ 'read_elasticsearch_config',
+ i18n.translate(
+ 'xpack.securitySolution.endpoint.hostDetails.policyResponse.read_elasticsearch_config',
+ { defaultMessage: 'Read ElasticSearch Config' }
+ ),
+ ],
+ [
+ 'read_events_config',
+ i18n.translate(
+ 'xpack.securitySolution.endpoint.hostDetails.policyResponse.read_events_config',
+ { defaultMessage: 'Read Events Config' }
+ ),
+ ],
+ [
+ 'read_kernel_config',
+ i18n.translate(
+ 'xpack.securitySolution.endpoint.hostDetails.policyResponse.read_kernel_config',
+ { defaultMessage: 'Read Kernel Config' }
+ ),
+ ],
+ [
+ 'read_logging_config',
+ i18n.translate(
+ 'xpack.securitySolution.endpoint.hostDetails.policyResponse.read_logging_config',
+ { defaultMessage: 'Read Logging Config' }
+ ),
+ ],
+ [
+ 'read_malware_config',
+ i18n.translate(
+ 'xpack.securitySolution.endpoint.hostDetails.policyResponse.read_malware_config',
+ { defaultMessage: 'Read Malware Config' }
+ ),
+ ],
+ [
+ 'workflow',
+ i18n.translate('xpack.securitySolution.endpoint.hostDetails.policyResponse.workflow', {
+ defaultMessage: 'Workflow',
+ }),
+ ],
+];
+
+const responseMap = new Map(policyResponses);
+
+// Additional values used in the Policy Response UI
responseMap.set(
'success',
i18n.translate('xpack.securitySolution.endpoint.hostDetails.policyResponse.success', {
@@ -49,144 +251,6 @@ responseMap.set(
defaultMessage: 'Events',
})
);
-responseMap.set(
- 'configure_elasticsearch_connection',
- i18n.translate(
- 'xpack.securitySolution.endpoint.hostDetails.policyResponse.configureElasticSearchConnection',
- {
- defaultMessage: 'Configure Elastic Search Connection',
- }
- )
-);
-responseMap.set(
- 'configure_logging',
- i18n.translate('xpack.securitySolution.endpoint.hostDetails.policyResponse.configureLogging', {
- defaultMessage: 'Configure Logging',
- })
-);
-responseMap.set(
- 'configure_kernel',
- i18n.translate('xpack.securitySolution.endpoint.hostDetails.policyResponse.configureKernel', {
- defaultMessage: 'Configure Kernel',
- })
-);
-responseMap.set(
- 'configure_malware',
- i18n.translate('xpack.securitySolution.endpoint.hostDetails.policyResponse.configureMalware', {
- defaultMessage: 'Configure Malware',
- })
-);
-responseMap.set(
- 'connect_kernel',
- i18n.translate('xpack.securitySolution.endpoint.hostDetails.policyResponse.connectKernel', {
- defaultMessage: 'Connect Kernel',
- })
-);
-responseMap.set(
- 'detect_file_open_events',
- i18n.translate(
- 'xpack.securitySolution.endpoint.hostDetails.policyResponse.detectFileOpenEvents',
- {
- defaultMessage: 'Detect File Open Events',
- }
- )
-);
-responseMap.set(
- 'detect_file_write_events',
- i18n.translate(
- 'xpack.securitySolution.endpoint.hostDetails.policyResponse.detectFileWriteEvents',
- {
- defaultMessage: 'Detect File Write Events',
- }
- )
-);
-responseMap.set(
- 'detect_image_load_events',
- i18n.translate(
- 'xpack.securitySolution.endpoint.hostDetails.policyResponse.detectImageLoadEvents',
- {
- defaultMessage: 'Detect Image Load Events',
- }
- )
-);
-responseMap.set(
- 'detect_process_events',
- i18n.translate('xpack.securitySolution.endpoint.hostDetails.policyResponse.detectProcessEvents', {
- defaultMessage: 'Detect Process Events',
- })
-);
-responseMap.set(
- 'download_global_artifacts',
- i18n.translate(
- 'xpack.securitySolution.endpoint.hostDetails.policyResponse.downloadGlobalArtifacts',
- {
- defaultMessage: 'Download Global Artifacts',
- }
- )
-);
-responseMap.set(
- 'load_config',
- i18n.translate('xpack.securitySolution.endpoint.hostDetails.policyResponse.loadConfig', {
- defaultMessage: 'Load Config',
- })
-);
-responseMap.set(
- 'load_malware_model',
- i18n.translate('xpack.securitySolution.endpoint.hostDetails.policyResponse.loadMalwareModel', {
- defaultMessage: 'Load Malware Model',
- })
-);
-responseMap.set(
- 'read_elasticsearch_config',
- i18n.translate(
- 'xpack.securitySolution.endpoint.hostDetails.policyResponse.readElasticSearchConfig',
- {
- defaultMessage: 'Read ElasticSearch Config',
- }
- )
-);
-responseMap.set(
- 'read_events_config',
- i18n.translate('xpack.securitySolution.endpoint.hostDetails.policyResponse.readEventsConfig', {
- defaultMessage: 'Read Events Config',
- })
-);
-responseMap.set(
- 'read_kernel_config',
- i18n.translate('xpack.securitySolution.endpoint.hostDetails.policyResponse.readKernelConfig', {
- defaultMessage: 'Read Kernel Config',
- })
-);
-responseMap.set(
- 'read_logging_config',
- i18n.translate('xpack.securitySolution.endpoint.hostDetails.policyResponse.readLoggingConfig', {
- defaultMessage: 'Read Logging Config',
- })
-);
-responseMap.set(
- 'read_malware_config',
- i18n.translate('xpack.securitySolution.endpoint.hostDetails.policyResponse.readMalwareConfig', {
- defaultMessage: 'Read Malware Config',
- })
-);
-responseMap.set(
- 'workflow',
- i18n.translate('xpack.securitySolution.endpoint.hostDetails.policyResponse.workflow', {
- defaultMessage: 'Workflow',
- })
-);
-responseMap.set(
- 'download_model',
- i18n.translate('xpack.securitySolution.endpoint.hostDetails.policyResponse.downloadModel', {
- defaultMessage: 'Download Model',
- })
-);
-responseMap.set(
- 'ingest_events_config',
- i18n.translate('xpack.securitySolution.endpoint.hostDetails.policyResponse.injestEventsConfig', {
- defaultMessage: 'Injest Events Config',
- })
-);
/**
* Maps a server provided value to corresponding i18n'd string.
@@ -195,5 +259,13 @@ export function formatResponse(responseString: string) {
if (responseMap.has(responseString)) {
return responseMap.get(responseString);
}
- return responseString;
+
+ // Its possible for the UI to receive an Action name that it does not yet have a translation,
+ // thus we generate a label for it here by making it more user fiendly
+ responseMap.set(
+ responseString,
+ responseString.replace(/_/g, ' ').replace(/\b(\w)/g, (m) => m.toUpperCase())
+ );
+
+ return responseMap.get(responseString);
}
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
index 996b987ea2be3..a61088e2edd29 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
@@ -13,8 +13,9 @@ import { mockPolicyResultList } from '../../policy/store/policy_list/mock_policy
import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint';
import {
HostInfo,
- HostStatus,
HostPolicyResponseActionStatus,
+ HostPolicyResponseAppliedAction,
+ HostStatus,
} from '../../../../../common/endpoint/types';
import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data';
import { AppAction } from '../../../../common/store/actions';
@@ -251,6 +252,16 @@ describe('when on the hosts page', () => {
) {
malwareResponseConfigurations.concerned_actions.push(downloadModelAction.name);
}
+
+ // Add an unknown Action Name - to ensure we handle the format of it on the UI
+ const unknownAction: HostPolicyResponseAppliedAction = {
+ status: HostPolicyResponseActionStatus.success,
+ message: 'test message',
+ name: 'a_new_unknown_action',
+ };
+ policyResponse.Endpoint.policy.applied.actions.push(unknownAction);
+ malwareResponseConfigurations.concerned_actions.push(unknownAction.name);
+
reactTestingLibrary.act(() => {
store.dispatch({
type: 'serverReturnedHostPolicyResponse',
@@ -564,6 +575,10 @@ describe('when on the hosts page', () => {
'?page_index=0&page_size=10&selected_host=1'
);
});
+
+ it('should format unknown policy action names', async () => {
+ expect(renderResult.getByText('A New Unknown Action')).not.toBeNull();
+ });
});
});
});
From 41c4f18b8961dcfe537c727c8547d28de7c8c501 Mon Sep 17 00:00:00 2001
From: Scotty Bollinger
Date: Mon, 13 Jul 2020 13:10:35 -0500
Subject: [PATCH 26/40] Workplace Search in Kibana MVP (#70979)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Add Workplace Search plugin to app
- Adds telemetry for Workplace Search
- Adds routing for telemetry and overview
- Registers plugin
* Add breadcrumbs for Workplace Search
* Add Workplace Search index
* Add route paths, types and shared assets
* Add shared Workplace Search components
* Add setup guide to Workplace Search
* Add error state to Workplace Search
* Add Workplace Search overview
This is the functional MVP for Workplace Search
* Update telemetry per recent changes
- Remove saved objects indexing
- add schema definition
- remove no_ws_account
- minor cleanup
* Fix pluralization syntax
- Still not working but fixed the syntax nonetheless
* Change pluralization method
- Was unable to get the `FormattedMessage` to work using the syntax in the docs. Always added ‘more’, even when there were zero (or one for users). This commit uses an alternative approach that works
* Update readme
* Fix duplicate i18n label
* Fix failing test from previous commit
:facepalm:
* Update link for image in Setup Guide
* Remove need for hash in routes
Because of a change in the Workplace Search rails code, we can now use non-hash routes that will be redirected by rails so that we don’t have users stuck on the overview page in Workplace Search when logging in
* Directly link to source details from activity feed
Previously the dashboard in legacy Workplace Search linked to the sources page and this was replicated in the Kibana MVP. This PR aligns with the legacy dashboard directly linking to the source details
https://github.com/elastic/ent-search/pull/1688
* Add warn logging to Workplace Search telemetry collector
* Change casing to camel to match App Search
* Misc security feedback for Workplace Search
* Update licence mocks to match App Search
* PR feedback from App Search PR
* REmove duplicate code from merge conflict
* Fix tests
* Move varible declaration inside map for TypeScript
There was no other way :facepalm:
* Refactor last commit
* Add punctuation
Smallest commit ever.
* Fix actionPath type errors
* Update rebase feedback
* Fix failing test
* Update telemetry test after AS PR feedback
* DRY out error state prompt copy
* DRY out telemetry endpoint into a single route + DRY out
DRY out endpoint
- Instead of /api/app_search/telemetry & /api/workplace_search/telemetry, just have a single /api/enterprise_search/telemetry endpoint that takes a product param
- Update public/send_telemetry accordingly (+ write tests for SendWorkplaceSearchTelemetry)
DRY out helpers
- Pull out certain reusable helper functions into a shared lib/ folder and have them take the repo id/name as a param
- Move tests over
- Remove misplaced comment block
+BONUS
- pull out content type header that's been giving us grief in Chrome into a constant
* Remove unused telemetry type
* Minor server cleanup - DRY out mockLogger
* Setup Guide cleanup
* Clean up Loading component
- use EUI vars per feedback
- remove unnecessary wrapper
- adjust vh for Kibana layout
- Actually apply loadingSpinner styles
* Misc i18n fixes
+ minor newline reduction, because prettier lets me
* Refactor Recent Activity component/styles
- Remove table markup/styles - not semantically correct or accessible in this case - replace w flex
- Fix link colors not inheriting
- Add EuiPanel, error colors looked odd against page background
- Fix prop/type definition
- CSS cleanup - EUI vars, correct BEM, don't target generic selectors
* [Opinionated] Refactor RecentActivity component
- Pull out iterated activity items into a child subcomponent
- Move constants/strings closer to where they're being used, instead of having to jump around the file
- Move IActivityFeed definition to this file, since that's primarily where it's used
@scottybollinger - if you're not a fan of this commit no worries, just let me know and we can discuss/roll back as needed
* Refactor ViewContentHeader
- remove unused CSS
- fallback cleanup
- refactor tests
* Refactor ContentSection
- Remove unused CSS classes
- Refactor tests to include all props/more specific assertions
* Refactor StatisticCard
- Prefer using EuiTextColor to spans / custom classes
- Prefer using EuiCard's native `href` behavior over using our own wrapping link/--isClickablec class
- Note that when we port the link/destination over to React Router, we should instead opt to use React Router history, which will involve creating a EuiCard helper
- Make test a bit more specific
* Minor OrganizationStats cleanup
- Use EuiFlexGrid
* Refactor OnboardingSteps
- i18n
- Compact i18n newlines (nit)
- Convert FormattedMessage to i18n.translate for easier test assertions
- Org Name CTA
- Move to separate child subcomponent to make it easier to quickly skim the parent container
- Remove unused CSS class
- Fix/add responsive behavior
- Tests refactor
- Use describe() blocks to break up tests by card/section
- Make sure each card has tests for each state - zero, some/complete, and disabled/no access
- Assert by plain text now that we're using i18n.translate()
- Remove ContentSection/EuiPanel assertions - they're not terribly useful, and we have more specific elements to check
- Add accounts={0} test to satisfy yellow branch line
* Clean up OnboardingCard
- Remove unused CSS class
- Remove unnecessary template literal
Tests
- Swap out check for EuiFlexItem - it's not really the content we're concerned about displaying, EuiEmptyPrompt is the primary component
- Remove need for mount() by dive()ing into EuiEmptyPrompt (this also removes the need to specify a[data-test-subj] instead of just [data-test-subj])
- Simplify empty button test - previous test has already checked for href/telemetry
- Cover uncovered actionPath branch line
* Minor Overview cleanup
- Remove unused telemetry type
- Remove unused CSS class
- finally
- Remove unused license context from tests
* Feedback: UI fixes
- Fix setup guide CSS class casing
- Remove border transparent (UX > UI)
* Fix Workplace Search not being hidden on feature control
- Whoops, totally missed this :facepalm:
* Add very basic functional Workplace Search test
- Has to be without_host_configured, since with host requires Enterprise Search
- Just checks for basic Setup Guide redirect for now
- TODO: Add more in-depth feature/privilege functional tests for both plugins at later date
* Pay down test render/loading tech debt
- Turns out you don't need render(), shallow() skips useEffect already :facepalm:
- Fix outdated comment import example
* DRY out repeated mountWithApiMock into mountWithAsyncContext
+ Minor engines_overview test refactors:
- Prefer to define `const wrapper` at the start of each test rather than a `let wrapper` - this better for sandboxing / not leaking state between tests
- Move Platinum license tests above pagination, so the contrast between the two tests are easier to grok
* Design feedback
- README copy tweak + linting
- Remove unused euiCard classes from onboarding card
Co-authored-by: Constance Chen
---
x-pack/plugins/enterprise_search/README.md | 5 +-
.../enterprise_search/common/constants.ts | 2 +
.../public/applications/__mocks__/index.ts | 6 +-
.../__mocks__/mount_with_context.mock.tsx | 33 +++-
.../__mocks__/shallow_usecontext.mock.ts | 2 +-
.../empty_states/empty_states.test.tsx | 3 +-
.../components/empty_states/error_state.tsx | 74 +-------
.../engine_overview/engine_overview.test.tsx | 145 ++++++--------
.../public/applications/index.test.tsx | 10 +-
.../error_state/error_state_prompt.test.tsx | 21 ++
.../shared/error_state/error_state_prompt.tsx | 79 ++++++++
.../applications/shared/error_state/index.ts | 7 +
.../generate_breadcrumbs.test.ts | 85 ++++++++-
.../generate_breadcrumbs.ts | 3 +
.../shared/kibana_breadcrumbs/index.ts | 9 +-
.../kibana_breadcrumbs/set_breadcrumbs.tsx | 29 ++-
.../applications/shared/telemetry/index.ts | 1 +
.../shared/telemetry/send_telemetry.test.tsx | 25 ++-
.../shared/telemetry/send_telemetry.tsx | 19 +-
.../public/applications/shared/types.ts | 14 ++
.../assets/getting_started.png | Bin 0 -> 487510 bytes
.../workplace_search/assets/logo.svg | 5 +
.../error_state/error_state.test.tsx | 21 ++
.../components/error_state/error_state.tsx | 34 ++++
.../components/error_state/index.ts | 7 +
.../components/overview/index.ts | 7 +
.../overview/onboarding_card.test.tsx | 54 ++++++
.../components/overview/onboarding_card.tsx | 92 +++++++++
.../overview/onboarding_steps.test.tsx | 136 +++++++++++++
.../components/overview/onboarding_steps.tsx | 179 ++++++++++++++++++
.../overview/organization_stats.test.tsx | 31 +++
.../overview/organization_stats.tsx | 74 ++++++++
.../components/overview/overview.test.tsx | 77 ++++++++
.../components/overview/overview.tsx | 151 +++++++++++++++
.../components/overview/recent_activity.scss | 37 ++++
.../overview/recent_activity.test.tsx | 61 ++++++
.../components/overview/recent_activity.tsx | 131 +++++++++++++
.../overview/statistic_card.test.tsx | 32 ++++
.../components/overview/statistic_card.tsx | 46 +++++
.../components/setup_guide/index.ts | 7 +
.../setup_guide/setup_guide.test.tsx | 21 ++
.../components/setup_guide/setup_guide.tsx | 70 +++++++
.../components/shared/assets/share_circle.svg | 3 +
.../content_section/content_section.test.tsx | 50 +++++
.../content_section/content_section.tsx | 45 +++++
.../shared/content_section/index.ts | 7 +
.../components/shared/loading/index.ts | 7 +
.../components/shared/loading/loading.scss | 14 ++
.../shared/loading/loading.test.tsx | 21 ++
.../components/shared/loading/loading.tsx | 17 ++
.../components/shared/product_button/index.ts | 7 +
.../product_button/product_button.test.tsx | 38 ++++
.../shared/product_button/product_button.tsx | 41 ++++
.../components/shared/use_routes/index.ts | 7 +
.../shared/use_routes/use_routes.tsx | 15 ++
.../shared/view_content_header/index.ts | 7 +
.../view_content_header.test.tsx | 39 ++++
.../view_content_header.tsx | 42 ++++
.../workplace_search/index.test.tsx | 46 +++++
.../applications/workplace_search/index.tsx | 29 +++
.../applications/workplace_search/routes.ts | 12 ++
.../applications/workplace_search/types.ts | 16 ++
.../enterprise_search/public/plugin.ts | 29 ++-
.../collectors/app_search/telemetry.test.ts | 49 +----
.../server/collectors/app_search/telemetry.ts | 55 +-----
.../server/collectors/lib/telemetry.test.ts | 69 +++++++
.../server/collectors/lib/telemetry.ts | 62 ++++++
.../workplace_search/telemetry.test.ts | 101 ++++++++++
.../collectors/workplace_search/telemetry.ts | 115 +++++++++++
.../enterprise_search/server/plugin.ts | 31 +--
.../telemetry.test.ts | 81 ++++++--
.../telemetry.ts | 26 ++-
.../routes/workplace_search/overview.test.ts | 127 +++++++++++++
.../routes/workplace_search/overview.ts | 46 +++++
.../workplace_search/telemetry.ts | 19 ++
.../schema/xpack_plugins.json | 37 ++++
.../app_search/setup_guide.ts | 2 +-
.../without_host_configured/index.ts | 1 +
.../workplace_search/setup_guide.ts | 36 ++++
.../page_objects/index.ts | 2 +
.../page_objects/workplace_search.ts | 17 ++
.../security_and_spaces/tests/catalogue.ts | 3 +-
.../security_and_spaces/tests/nav_links.ts | 8 +-
.../security_only/tests/catalogue.ts | 3 +-
.../security_only/tests/nav_links.ts | 2 +-
85 files changed, 2908 insertions(+), 321 deletions(-)
create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/error_state/index.ts
create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/types.ts
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/assets/getting_started.png
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/assets/logo.svg
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/error_state/error_state.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/error_state/error_state.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/error_state/index.ts
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/index.ts
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_card.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_card.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/organization_stats.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/organization_stats.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.scss
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/statistic_card.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/statistic_card.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/setup_guide/index.ts
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/setup_guide/setup_guide.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/setup_guide/setup_guide.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/share_circle.svg
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/index.ts
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/loading/index.ts
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/loading/loading.scss
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/loading/loading.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/loading/loading.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/index.ts
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/use_routes/index.ts
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/use_routes/use_routes.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/index.ts
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts
create mode 100644 x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.test.ts
create mode 100644 x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.ts
create mode 100644 x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.test.ts
create mode 100644 x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.ts
rename x-pack/plugins/enterprise_search/server/routes/{app_search => enterprise_search}/telemetry.test.ts (56%)
rename x-pack/plugins/enterprise_search/server/routes/{app_search => enterprise_search}/telemetry.ts (55%)
create mode 100644 x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts
create mode 100644 x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.ts
create mode 100644 x-pack/plugins/enterprise_search/server/saved_objects/workplace_search/telemetry.ts
create mode 100644 x-pack/test/functional_enterprise_search/apps/enterprise_search/without_host_configured/workplace_search/setup_guide.ts
create mode 100644 x-pack/test/functional_enterprise_search/page_objects/workplace_search.ts
diff --git a/x-pack/plugins/enterprise_search/README.md b/x-pack/plugins/enterprise_search/README.md
index 8c316c848184b..31ee304fe2247 100644
--- a/x-pack/plugins/enterprise_search/README.md
+++ b/x-pack/plugins/enterprise_search/README.md
@@ -2,7 +2,10 @@
## Overview
-This plugin's goal is to provide a Kibana user interface to the Enterprise Search solution's products (App Search and Workplace Search). In its current MVP state, the plugin provides a basic engines overview from App Search with the goal of gathering user feedback and raising product awareness.
+This plugin's goal is to provide a Kibana user interface to the Enterprise Search solution's products (App Search and Workplace Search). In it's current MVP state, the plugin provides the following with the goal of gathering user feedback and raising product awareness:
+
+- **App Search:** A basic engines overview with links into the product.
+- **Workplace Search:** A simple app overview with basic statistics, links to the sources, users (if standard auth), and product settings.
## Development
diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts
index c134131caba75..fc9a47717871b 100644
--- a/x-pack/plugins/enterprise_search/common/constants.ts
+++ b/x-pack/plugins/enterprise_search/common/constants.ts
@@ -4,4 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
+export const JSON_HEADER = { 'Content-Type': 'application/json' }; // This needs specific casing or Chrome throws a 415 error
+
export const ENGINES_PAGE_SIZE = 10;
diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts
index 14fde357a980a..6f82946c0ea14 100644
--- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts
@@ -7,7 +7,11 @@
export { mockHistory } from './react_router_history.mock';
export { mockKibanaContext } from './kibana_context.mock';
export { mockLicenseContext } from './license_context.mock';
-export { mountWithContext, mountWithKibanaContext } from './mount_with_context.mock';
+export {
+ mountWithContext,
+ mountWithKibanaContext,
+ mountWithAsyncContext,
+} from './mount_with_context.mock';
export { shallowWithIntl } from './shallow_with_i18n.mock';
// Note: shallow_usecontext must be imported directly as a file
diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx
index dfcda544459d4..1e0df1326c177 100644
--- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx
@@ -5,7 +5,8 @@
*/
import React from 'react';
-import { mount } from 'enzyme';
+import { act } from 'react-dom/test-utils';
+import { mount, ReactWrapper } from 'enzyme';
import { I18nProvider } from '@kbn/i18n/react';
import { KibanaContext } from '../';
@@ -47,3 +48,33 @@ export const mountWithKibanaContext = (children: React.ReactNode, context?: obje
);
};
+
+/**
+ * This helper is intended for components that have async effects
+ * (e.g. http fetches) on mount. It mostly adds act/update boilerplate
+ * that's needed for the wrapper to play nice with Enzyme/Jest
+ *
+ * Example usage:
+ *
+ * const wrapper = mountWithAsyncContext(, { http: { get: () => someData } });
+ */
+export const mountWithAsyncContext = async (
+ children: React.ReactNode,
+ context: object
+): Promise => {
+ let wrapper: ReactWrapper | undefined;
+
+ // We get a lot of act() warning/errors in the terminal without this.
+ // TBH, I don't fully understand why since Enzyme's mount is supposed to
+ // have act() baked in - could be because of the wrapping context provider?
+ await act(async () => {
+ wrapper = mountWithContext(children, context);
+ });
+ if (wrapper) {
+ wrapper.update(); // This seems to be required for the DOM to actually update
+
+ return wrapper;
+ } else {
+ throw new Error('Could not mount wrapper');
+ }
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts
index 767a52a75d1fb..2bcdd42c38055 100644
--- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts
@@ -19,7 +19,7 @@ jest.mock('react', () => ({
/**
* Example usage within a component test using shallow():
*
- * import '../../../test_utils/mock_shallow_usecontext'; // Must come before React's import, adjust relative path as needed
+ * import '../../../__mocks__/shallow_usecontext'; // Must come before React's import, adjust relative path as needed
*
* import React from 'react';
* import { shallow } from 'enzyme';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.test.tsx
index 12bf003564103..25a9fa7430c40 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.test.tsx
@@ -9,6 +9,7 @@ import '../../../__mocks__/shallow_usecontext.mock';
import React from 'react';
import { shallow } from 'enzyme';
import { EuiEmptyPrompt, EuiButton, EuiLoadingContent } from '@elastic/eui';
+import { ErrorStatePrompt } from '../../../shared/error_state';
jest.mock('../../../shared/telemetry', () => ({
sendTelemetry: jest.fn(),
@@ -22,7 +23,7 @@ describe('ErrorState', () => {
it('renders', () => {
const wrapper = shallow();
- expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
+ expect(wrapper.find(ErrorStatePrompt)).toHaveLength(1);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx
index d8eeff2aba1c6..7ac02082ee75c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx
@@ -4,21 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useContext } from 'react';
-import { EuiPage, EuiPageBody, EuiPageContent, EuiEmptyPrompt, EuiCode } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
+import React from 'react';
+import { EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui';
-import { EuiButton } from '../../../shared/react_router_helpers';
+import { ErrorStatePrompt } from '../../../shared/error_state';
import { SetAppSearchBreadcrumbs as SetBreadcrumbs } from '../../../shared/kibana_breadcrumbs';
import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry';
-import { KibanaContext, IKibanaContext } from '../../../index';
import { EngineOverviewHeader } from '../engine_overview_header';
import './empty_states.scss';
export const ErrorState: React.FC = () => {
- const { enterpriseSearchUrl } = useContext(KibanaContext) as IKibanaContext;
-
return (
@@ -26,68 +22,8 @@ export const ErrorState: React.FC = () => {
-
-
-
-
- }
- titleSize="l"
- body={
- <>
-
- {enterpriseSearchUrl},
- }}
- />
-
-
- -
- config/kibana.yml,
- }}
- />
-
- -
-
-
- -
- [enterpriseSearch][plugins],
- }}
- />
-
-
- >
- }
- actions={
-
-
-
- }
- />
+
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx
index 4d2a2ea1df9aa..45ab5dc5b9ab1 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx
@@ -8,51 +8,45 @@ import '../../../__mocks__/react_router_history.mock';
import React from 'react';
import { act } from 'react-dom/test-utils';
-import { render, ReactWrapper } from 'enzyme';
+import { shallow, ReactWrapper } from 'enzyme';
-import { I18nProvider } from '@kbn/i18n/react';
-import { KibanaContext } from '../../../';
-import { LicenseContext } from '../../../shared/licensing';
-import { mountWithContext, mockKibanaContext } from '../../../__mocks__';
+import { mountWithAsyncContext, mockKibanaContext } from '../../../__mocks__';
-import { EmptyState, ErrorState } from '../empty_states';
-import { EngineTable, IEngineTablePagination } from './engine_table';
+import { LoadingState, EmptyState, ErrorState } from '../empty_states';
+import { EngineTable } from './engine_table';
import { EngineOverview } from './';
describe('EngineOverview', () => {
+ const mockHttp = mockKibanaContext.http;
+
describe('non-happy-path states', () => {
it('isLoading', () => {
- // We use render() instead of mount() here to not trigger lifecycle methods (i.e., useEffect)
- // TODO: Consider pulling this out to a renderWithContext mock/helper
- const wrapper: Cheerio = render(
-
-
-
-
-
-
-
- );
-
- // render() directly renders HTML which means we have to look for selectors instead of for LoadingState directly
- expect(wrapper.find('.euiLoadingContent')).toHaveLength(2);
+ const wrapper = shallow();
+
+ expect(wrapper.find(LoadingState)).toHaveLength(1);
});
it('isEmpty', async () => {
- const wrapper = await mountWithApiMock({
- get: () => ({
- results: [],
- meta: { page: { total_results: 0 } },
- }),
+ const wrapper = await mountWithAsyncContext(, {
+ http: {
+ ...mockHttp,
+ get: () => ({
+ results: [],
+ meta: { page: { total_results: 0 } },
+ }),
+ },
});
expect(wrapper.find(EmptyState)).toHaveLength(1);
});
it('hasErrorConnecting', async () => {
- const wrapper = await mountWithApiMock({
- get: () => ({ invalidPayload: true }),
+ const wrapper = await mountWithAsyncContext(, {
+ http: {
+ ...mockHttp,
+ get: () => ({ invalidPayload: true }),
+ },
});
expect(wrapper.find(ErrorState)).toHaveLength(1);
});
@@ -78,17 +72,17 @@ describe('EngineOverview', () => {
},
};
const mockApi = jest.fn(() => mockedApiResponse);
- let wrapper: ReactWrapper;
- beforeAll(async () => {
- wrapper = await mountWithApiMock({ get: mockApi });
+ beforeEach(() => {
+ jest.clearAllMocks();
});
- it('renders', () => {
- expect(wrapper.find(EngineTable)).toHaveLength(1);
- });
+ it('renders and calls the engines API', async () => {
+ const wrapper = await mountWithAsyncContext(, {
+ http: { ...mockHttp, get: mockApi },
+ });
- it('calls the engines API', () => {
+ expect(wrapper.find(EngineTable)).toHaveLength(1);
expect(mockApi).toHaveBeenNthCalledWith(1, '/api/app_search/engines', {
query: {
type: 'indexed',
@@ -97,19 +91,42 @@ describe('EngineOverview', () => {
});
});
+ describe('when on a platinum license', () => {
+ it('renders a 2nd meta engines table & makes a 2nd meta engines API call', async () => {
+ const wrapper = await mountWithAsyncContext(, {
+ http: { ...mockHttp, get: mockApi },
+ license: { type: 'platinum', isActive: true },
+ });
+
+ expect(wrapper.find(EngineTable)).toHaveLength(2);
+ expect(mockApi).toHaveBeenNthCalledWith(2, '/api/app_search/engines', {
+ query: {
+ type: 'meta',
+ pageIndex: 1,
+ },
+ });
+ });
+ });
+
describe('pagination', () => {
- const getTablePagination: () => IEngineTablePagination = () =>
- wrapper.find(EngineTable).first().prop('pagination');
+ const getTablePagination = (wrapper: ReactWrapper) =>
+ wrapper.find(EngineTable).prop('pagination');
- it('passes down page data from the API', () => {
- const pagination = getTablePagination();
+ it('passes down page data from the API', async () => {
+ const wrapper = await mountWithAsyncContext(, {
+ http: { ...mockHttp, get: mockApi },
+ });
+ const pagination = getTablePagination(wrapper);
expect(pagination.totalEngines).toEqual(100);
expect(pagination.pageIndex).toEqual(0);
});
it('re-polls the API on page change', async () => {
- await act(async () => getTablePagination().onPaginate(5));
+ const wrapper = await mountWithAsyncContext(, {
+ http: { ...mockHttp, get: mockApi },
+ });
+ await act(async () => getTablePagination(wrapper).onPaginate(5));
wrapper.update();
expect(mockApi).toHaveBeenLastCalledWith('/api/app_search/engines', {
@@ -118,54 +135,8 @@ describe('EngineOverview', () => {
pageIndex: 5,
},
});
- expect(getTablePagination().pageIndex).toEqual(4);
- });
- });
-
- describe('when on a platinum license', () => {
- beforeAll(async () => {
- mockApi.mockClear();
- wrapper = await mountWithApiMock({
- license: { type: 'platinum', isActive: true },
- get: mockApi,
- });
- });
-
- it('renders a 2nd meta engines table', () => {
- expect(wrapper.find(EngineTable)).toHaveLength(2);
- });
-
- it('makes a 2nd call to the engines API with type meta', () => {
- expect(mockApi).toHaveBeenNthCalledWith(2, '/api/app_search/engines', {
- query: {
- type: 'meta',
- pageIndex: 1,
- },
- });
+ expect(getTablePagination(wrapper).pageIndex).toEqual(4);
});
});
});
-
- /**
- * Test helpers
- */
-
- const mountWithApiMock = async ({ get, license }: { get(): any; license?: object }) => {
- let wrapper: ReactWrapper | undefined;
- const httpMock = { ...mockKibanaContext.http, get };
-
- // We get a lot of act() warning/errors in the terminal without this.
- // TBH, I don't fully understand why since Enzyme's mount is supposed to
- // have act() baked in - could be because of the wrapping context provider?
- await act(async () => {
- wrapper = mountWithContext(, { http: httpMock, license });
- });
- if (wrapper) {
- wrapper.update(); // This seems to be required for the DOM to actually update
-
- return wrapper;
- } else {
- throw new Error('Could not mount wrapper');
- }
- };
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx
index 1aead8468ca3b..70e16e61846b4 100644
--- a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx
@@ -6,14 +6,16 @@
import React from 'react';
+import { AppMountParameters } from 'src/core/public';
import { coreMock } from 'src/core/public/mocks';
import { licensingMock } from '../../../licensing/public/mocks';
import { renderApp } from './';
import { AppSearch } from './app_search';
+import { WorkplaceSearch } from './workplace_search';
describe('renderApp', () => {
- const params = coreMock.createAppMountParamters();
+ let params: AppMountParameters;
const core = coreMock.createStart();
const config = {};
const plugins = {
@@ -22,6 +24,7 @@ describe('renderApp', () => {
beforeEach(() => {
jest.clearAllMocks();
+ params = coreMock.createAppMountParamters();
});
it('mounts and unmounts UI', () => {
@@ -37,4 +40,9 @@ describe('renderApp', () => {
renderApp(AppSearch, core, params, config, plugins);
expect(params.element.querySelector('.setupGuide')).not.toBeNull();
});
+
+ it('renders WorkplaceSearch', () => {
+ renderApp(WorkplaceSearch, core, params, config, plugins);
+ expect(params.element.querySelector('.setupGuide')).not.toBeNull();
+ });
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx
new file mode 100644
index 0000000000000..29b773b80158a
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx
@@ -0,0 +1,21 @@
+/*
+ * 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 '../../__mocks__/shallow_usecontext.mock';
+
+import React from 'react';
+import { shallow } from 'enzyme';
+import { EuiEmptyPrompt } from '@elastic/eui';
+
+import { ErrorStatePrompt } from './';
+
+describe('ErrorState', () => {
+ it('renders', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx
new file mode 100644
index 0000000000000..81455cea0b497
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx
@@ -0,0 +1,79 @@
+/*
+ * 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, { useContext } from 'react';
+import { EuiEmptyPrompt, EuiCode } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { EuiButton } from '../react_router_helpers';
+import { KibanaContext, IKibanaContext } from '../../index';
+
+export const ErrorStatePrompt: React.FC = () => {
+ const { enterpriseSearchUrl } = useContext(KibanaContext) as IKibanaContext;
+
+ return (
+
+
+
+ }
+ titleSize="l"
+ body={
+ <>
+
+ {enterpriseSearchUrl},
+ }}
+ />
+
+
+ -
+ config/kibana.yml,
+ }}
+ />
+
+ -
+
+
+ -
+ [enterpriseSearch][plugins],
+ }}
+ />
+
+
+ >
+ }
+ actions={
+
+
+
+ }
+ />
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/index.ts
new file mode 100644
index 0000000000000..1012fdf4126a2
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { ErrorStatePrompt } from './error_state_prompt';
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.test.ts
index 7ea73577c4de6..70aa723d62601 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.test.ts
@@ -5,7 +5,7 @@
*/
import { generateBreadcrumb } from './generate_breadcrumbs';
-import { appSearchBreadcrumbs, enterpriseSearchBreadcrumbs } from './';
+import { appSearchBreadcrumbs, enterpriseSearchBreadcrumbs, workplaceSearchBreadcrumbs } from './';
import { mockHistory as mockHistoryUntyped } from '../../__mocks__';
const mockHistory = mockHistoryUntyped as any;
@@ -204,3 +204,86 @@ describe('appSearchBreadcrumbs', () => {
});
});
});
+
+describe('workplaceSearchBreadcrumbs', () => {
+ const breadCrumbs = [
+ {
+ text: 'Page 1',
+ path: '/page1',
+ },
+ {
+ text: 'Page 2',
+ path: '/page2',
+ },
+ ];
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockHistory.createHref.mockImplementation(
+ ({ pathname }: any) => `/enterprise_search/workplace_search${pathname}`
+ );
+ });
+
+ const subject = () => workplaceSearchBreadcrumbs(mockHistory)(breadCrumbs);
+
+ it('Builds a chain of breadcrumbs with Enterprise Search and Workplace Search at the root', () => {
+ expect(subject()).toEqual([
+ {
+ text: 'Enterprise Search',
+ },
+ {
+ href: '/enterprise_search/workplace_search/',
+ onClick: expect.any(Function),
+ text: 'Workplace Search',
+ },
+ {
+ href: '/enterprise_search/workplace_search/page1',
+ onClick: expect.any(Function),
+ text: 'Page 1',
+ },
+ {
+ href: '/enterprise_search/workplace_search/page2',
+ onClick: expect.any(Function),
+ text: 'Page 2',
+ },
+ ]);
+ });
+
+ it('shows just the root if breadcrumbs is empty', () => {
+ expect(workplaceSearchBreadcrumbs(mockHistory)()).toEqual([
+ {
+ text: 'Enterprise Search',
+ },
+ {
+ href: '/enterprise_search/workplace_search/',
+ onClick: expect.any(Function),
+ text: 'Workplace Search',
+ },
+ ]);
+ });
+
+ describe('links', () => {
+ const eventMock = {
+ preventDefault: jest.fn(),
+ } as any;
+
+ it('has Enterprise Search text first', () => {
+ expect(subject()[0].onClick).toBeUndefined();
+ });
+
+ it('has a link to Workplace Search second', () => {
+ (subject()[1] as any).onClick(eventMock);
+ expect(mockHistory.push).toHaveBeenCalledWith('/');
+ });
+
+ it('has a link to page 1 third', () => {
+ (subject()[2] as any).onClick(eventMock);
+ expect(mockHistory.push).toHaveBeenCalledWith('/page1');
+ });
+
+ it('has a link to page 2 last', () => {
+ (subject()[3] as any).onClick(eventMock);
+ expect(mockHistory.push).toHaveBeenCalledWith('/page2');
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.ts
index 8f72875a32bd4..b57fdfdbb75ca 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.ts
@@ -52,3 +52,6 @@ export const enterpriseSearchBreadcrumbs = (history: History) => (
export const appSearchBreadcrumbs = (history: History) => (breadcrumbs: TBreadcrumbs = []) =>
enterpriseSearchBreadcrumbs(history)([{ text: 'App Search', path: '/' }, ...breadcrumbs]);
+
+export const workplaceSearchBreadcrumbs = (history: History) => (breadcrumbs: TBreadcrumbs = []) =>
+ enterpriseSearchBreadcrumbs(history)([{ text: 'Workplace Search', path: '/' }, ...breadcrumbs]);
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/index.ts
index cf8bbbc593f2f..c4ef68704b7e0 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/index.ts
@@ -4,6 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { enterpriseSearchBreadcrumbs } from './generate_breadcrumbs';
-export { appSearchBreadcrumbs } from './generate_breadcrumbs';
-export { SetAppSearchBreadcrumbs } from './set_breadcrumbs';
+export {
+ enterpriseSearchBreadcrumbs,
+ appSearchBreadcrumbs,
+ workplaceSearchBreadcrumbs,
+} from './generate_breadcrumbs';
+export { SetAppSearchBreadcrumbs, SetWorkplaceSearchBreadcrumbs } from './set_breadcrumbs';
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.tsx
index 530117e197616..e54f1a12b73cb 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.tsx
@@ -8,7 +8,11 @@ import React, { useContext, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { EuiBreadcrumb } from '@elastic/eui';
import { KibanaContext, IKibanaContext } from '../../index';
-import { appSearchBreadcrumbs, TBreadcrumbs } from './generate_breadcrumbs';
+import {
+ appSearchBreadcrumbs,
+ workplaceSearchBreadcrumbs,
+ TBreadcrumbs,
+} from './generate_breadcrumbs';
/**
* Small on-mount helper for setting Kibana's chrome breadcrumbs on any App Search view
@@ -17,19 +21,17 @@ import { appSearchBreadcrumbs, TBreadcrumbs } from './generate_breadcrumbs';
export type TSetBreadcrumbs = (breadcrumbs: EuiBreadcrumb[]) => void;
-interface IBreadcrumbProps {
+interface IBreadcrumbsProps {
text: string;
isRoot?: never;
}
-interface IRootBreadcrumbProps {
+interface IRootBreadcrumbsProps {
isRoot: true;
text?: never;
}
+type TBreadcrumbsProps = IBreadcrumbsProps | IRootBreadcrumbsProps;
-export const SetAppSearchBreadcrumbs: React.FC = ({
- text,
- isRoot,
-}) => {
+export const SetAppSearchBreadcrumbs: React.FC = ({ text, isRoot }) => {
const history = useHistory();
const { setBreadcrumbs } = useContext(KibanaContext) as IKibanaContext;
@@ -41,3 +43,16 @@ export const SetAppSearchBreadcrumbs: React.FC = ({ text, isRoot }) => {
+ const history = useHistory();
+ const { setBreadcrumbs } = useContext(KibanaContext) as IKibanaContext;
+
+ const crumb = isRoot ? [] : [{ text, path: history.location.pathname }];
+
+ useEffect(() => {
+ setBreadcrumbs(workplaceSearchBreadcrumbs(history)(crumb as TBreadcrumbs | []));
+ }, []);
+
+ return null;
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/index.ts
index f871f48b17154..eadf7fa805590 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/index.ts
@@ -6,3 +6,4 @@
export { sendTelemetry } from './send_telemetry';
export { SendAppSearchTelemetry } from './send_telemetry';
+export { SendWorkplaceSearchTelemetry } from './send_telemetry';
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx
index 9825c0d8ab889..3c873dbc25e37 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx
@@ -7,8 +7,10 @@
import React from 'react';
import { httpServiceMock } from 'src/core/public/mocks';
+import { JSON_HEADER as headers } from '../../../../common/constants';
import { mountWithKibanaContext } from '../../__mocks__';
-import { sendTelemetry, SendAppSearchTelemetry } from './';
+
+import { sendTelemetry, SendAppSearchTelemetry, SendWorkplaceSearchTelemetry } from './';
describe('Shared Telemetry Helpers', () => {
const httpMock = httpServiceMock.createSetupContract();
@@ -27,8 +29,8 @@ describe('Shared Telemetry Helpers', () => {
});
expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/telemetry', {
- headers: { 'Content-Type': 'application/json' },
- body: '{"action":"viewed","metric":"setup_guide"}',
+ headers,
+ body: '{"product":"enterprise_search","action":"viewed","metric":"setup_guide"}',
});
});
@@ -47,9 +49,20 @@ describe('Shared Telemetry Helpers', () => {
http: httpMock,
});
- expect(httpMock.put).toHaveBeenCalledWith('/api/app_search/telemetry', {
- headers: { 'Content-Type': 'application/json' },
- body: '{"action":"clicked","metric":"button"}',
+ expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/telemetry', {
+ headers,
+ body: '{"product":"app_search","action":"clicked","metric":"button"}',
+ });
+ });
+
+ it('SendWorkplaceSearchTelemetry component', () => {
+ mountWithKibanaContext(, {
+ http: httpMock,
+ });
+
+ expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/telemetry', {
+ headers,
+ body: '{"product":"workplace_search","action":"viewed","metric":"page"}',
});
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx
index 300cb18272717..715d61b31512c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx
@@ -7,6 +7,7 @@
import React, { useContext, useEffect } from 'react';
import { HttpSetup } from 'src/core/public';
+import { JSON_HEADER as headers } from '../../../../common/constants';
import { KibanaContext, IKibanaContext } from '../../index';
interface ISendTelemetryProps {
@@ -25,10 +26,8 @@ interface ISendTelemetry extends ISendTelemetryProps {
export const sendTelemetry = async ({ http, product, action, metric }: ISendTelemetry) => {
try {
- await http.put(`/api/${product}/telemetry`, {
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ action, metric }),
- });
+ const body = JSON.stringify({ product, action, metric });
+ await http.put('/api/enterprise_search/telemetry', { headers, body });
} catch (error) {
throw new Error('Unable to send telemetry');
}
@@ -36,7 +35,7 @@ export const sendTelemetry = async ({ http, product, action, metric }: ISendTele
/**
* React component helpers - useful for on-page-load/views
- * TODO: SendWorkplaceSearchTelemetry and SendEnterpriseSearchTelemetry
+ * TODO: SendEnterpriseSearchTelemetry
*/
export const SendAppSearchTelemetry: React.FC = ({ action, metric }) => {
@@ -48,3 +47,13 @@ export const SendAppSearchTelemetry: React.FC = ({ action,
return null;
};
+
+export const SendWorkplaceSearchTelemetry: React.FC = ({ action, metric }) => {
+ const { http } = useContext(KibanaContext) as IKibanaContext;
+
+ useEffect(() => {
+ sendTelemetry({ http, action, metric, product: 'workplace_search' });
+ }, [action, metric, http]);
+
+ return null;
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts
new file mode 100644
index 0000000000000..3f28710d92295
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export interface IFlashMessagesProps {
+ info?: string[];
+ warning?: string[];
+ error?: string[];
+ success?: string[];
+ isWrapped?: boolean;
+ children?: React.ReactNode;
+}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/assets/getting_started.png b/x-pack/plugins/enterprise_search/public/applications/workplace_search/assets/getting_started.png
new file mode 100644
index 0000000000000000000000000000000000000000..b6267b6e2c48e614a6376dc8d64ff8279d836670
GIT binary patch
literal 487510
zcmcG#bx>U2x-E*ky9ak^+}(pqBOypbH}3B4?(R+p2=4B#!67&V4H5_>kUW0p?7iQu
zSNGmJ|Gcj1?zQF|bA02I`Bn9-)iIjtikN6*XfQA^n953W+AuJP3NSG6;V5u#BL~a=
z&2L|*&Ps-EFfg2S|GZ(nDCvym=w}bN=8U{v8(%Tti
zVFz)iHiuZ*IEvGL@9w9iwgHRN>hr4s)SP7@);3DMt`HqxbzKWzI}2ejt)v9Cn77Cq
zfdj-HMD6Wh@8~AtEl&GiydrPc{|s}{QvVmk-A@Go0$cjC0x
z?(WVaTwGpWUYuUMoKCJ*T-?IKZyY>aJUkq42o5(NM|Y4nhoc+azZv8pZWgXK&h9o&
zj@17!g3O&f+{J0%l>WC14$l9fb#(hznBD@$)BnMb|4jeK8)p$&R|v@6$yL|M$^PGo()>3IHP0J4HM1JX!p89*
zUY36}{c8zC4&)9Ir{w|g05|{w96W-$JUk+N+#>wK8~{NP0N}r&YHyVQ2DyX&XJ99=
zjit~31gfScqU`AA4sx`BD9ee{zKPp%%2v|V40Yc^+!aUsE9DMw5ALi!#APxZ`
z0YN^1Ifz>b4EZlzZ|mipEIj@R;6LmCyKuoy7H=H?iIE7m5Dzy$4;aiLXvxFP0p{TY
zaR~A83vqyW_yu_REO-C{Ake?rG+b@o_72GYe`Wng75I%2A^-qba&rrD0006!Z>m6V
zJltRqhoHF-#M06NEX)HCqNN5~h*&zgI)L6P#l`_-1>tgbw4$Z{f7Qr3**m$aIf36|
z&ilWPE6d7ix;k0f*uPDmHeZXnP9yZHZEjr{*A{$HWBum(9=LEic-7wtdpaQ)Mf{yQ;T|KFVb
z*WCZoz5WeDo13E
z@5p3NI~8>()y5v+$;O%U8!l}&HnNl|d^Vz`REaYJy|msjs=k2gn&Pa#qTET{&-2*5
z4)EB<{??9azg3A=<;$J2@{yw$;TKlUJ=r3ntjldx>xqSM<_K2(DCC*xr)*Y{b_MNzjMF`st99V!$
zNSG7Qbbax&e|-1vr;EN%vwEp?%}`o8I%wqCf~K6DrGlI)aW#v7F~^$Zr@>9mv02>F
zz(@%^se-$DD52D_7Z69wSf(O+>H(cY-?1^540~DD=nUc_Psh%Sxg#jg?Ehsdc;uO=
zr+iSm636#qE;Wkf8;T_NK5h(?
zts47>{RH)$Gdf4XOY<-XjBUqi!um~1WIN}yRaK4~`rv#y&mk@Zi_d?WKTkzRoOL$P
zXOo_2-v7A{Mcv%D+LqYaPz6WPeRZ_Ch)%ZzV`Jq0)Hk8qd*Wg6z_hbCybq{=KZfAX&?|J5#S>Pl>V%*ONdJtzTB@4ycl!RUZF$5
zR*O>fgl=9v7Ue)UHH&mI1osl3kbpjIw81dn^_aRn6u2YDFc%a~hL-%qd6j0T+YXyT
z+A$(mzNaLWkBSGB>*@D_%tn+)9#bO7B8S&TgJxJ|aC49f;2wI}H=L}Ipa_t5#
zRXsmwW+SIL))TK98tXHsShebfXUr;Ik;A8jz@$xpUAQibDE7x1jV=qD&GM*(b6OX-
zmeE2B9t_zV~OEf+@H
zBS>E^c3!pNd9ceGPN+X0D%qF-XhM``*I8uvrWYSe-ZPNT`JZJg8#4yoRhddoqkOFMH*v7{G|;65CUK)P
zYHMrqLQ}_~e3q}j@s!o(P(!1An4me8Nc?m$`rna3Mx0+%O9wX(rg3GK7Fz|Nw(bl-
z!(4htH;#`;0gr?9oIuPJn6wW}aUCV16n>}3o75@P-jWrf)*tY!@^Du%9a&hwpV>jlaKk#>ur{?fn-xSTPU
zATgbc=o574RQc0{8J9?5b?g}nG|zL191w^k-6oNqNJ2v{+%n`4RAWljL*1~F#VDL
z7nx^$dh$yuZai3ZeuKor=}4|HljuW2Iy{Qqqy0)as@bkS4l#xc-^IrTNx`6%^3;3T
zZl{ONZ!B&$UFM6-m7l$;vmcC-cRrRo|IH;NS7q(NaF)a(&!vq1N~Z^<`|FF4^BM+P
zl-;{2)i*6Utqs}1Xr-+dLgevf&*AuSIV_c>e-LnMEcjR*+U;meGEq_<(>U6PYow$_v*2!ri$O-_ljN
zHvZ66zn-3A*Lrc@k^CsQFiFwbv0B^)rl|0iuz^oIIMAx>FxP{Z6Ekk+-wY*UYED4o
zFlQ)g7)dDQ+&R_h-Isy&sg`*}KqhfIXr%`({-;^+Ehtg`)%pxg^1T?Y5G
z>tQn9V8J>s=n}zKbSSIJQW{h{aXVC!4o!C33TmE)4CFU|k%!tQvth+rQAH$#Z)8>5
zs&uiIOg6wWrJ)Ir|LTj&;X_i2gOA-5A{v3^O|l>pZ4>SjZ*YiQijXUpf*h-iCezu@
zlDA!u*Q}u4U``gNP&txZ5#zjD7)zq;g@Ff4V&Z4d0>lg%Jt50sK5{cmpLN6
zb)G+Q%yBu#i4;^)v1mbqtYztY^eL(6EInVq7;I`7qRxViF@g2#Xn1(OJQ$SP-@0aQ
zI^e83gnN^NPwI!raJ+X@8l2
z%e(ilHN-6X!&|i;k6L1b&Km+AMZ}21C6m1?3%UAIf9ApuPC~Iq*pk_^|AIyjyyzu#
z+5PdC`fD85-SX#c0OOb&aTQ2gf+8jFu;|uh+q7f3kBo?vDbWbGogQh_8q4*
z3IIR7&p6UDM$Kw|Wx7rZe7H!~>-1xuuQ^uqX{+S}=~DK!9n3K0djb=a8;MW$%7h{O
z`-dc%4b79gOmbVH!5%r9{s_;JN432kw&`R;V);f_t`mwp)bj+n+9BXasoHnuk
zRq}Y~F(ARIw)M5k1|x;vx-XqT3$+Zbi!L2xoV`FSM58Q84n-8pXdZ6}*1Yq@Xf$%qzO
zMjFksT@@%urOo|Ff*68hCYGN-kms&6kM|n|I>Kqo%wR54@v4(E;0FvS*@ar(6FJaB
z*sZtx`h`EgIBB;8O2rjZwbU0t;%;9@Dx7$b_lzSK-~`cJK^iJ$)n$Q|%O#C%Z7o3~
z8%#bchg$W>{Ql_tC#rhtd+JM(_R1iRs2j&KcZ>E6cF}~47-yGL
z(G%xJEO3S2i7&d^26g{;S4UGg?3M?{vg>n!=km-76z7>>ceSQh8UmmChhn`ZCMUC(OeViGKV?&Pu^iLB@7#DbG3{S
zmt=3g6T#r3nH!fa^tNeqs;m9)6ZG0@yX6vbn(je7v^$7
zUo0b9u_F$m$aEqDzehaJ7;2u*lFhU|=MjVBb%LKA*oIy5^tL~k-M<=>+wZr3a3ljc
zn2IyNM`(y8(W6vc$8ZZQg?R``sdeR3@g70S;c9^jRz*J-=_L97AR(`MUCYke?
zbfVJQ*0uaprxC1uf-$~S+Vms#3{C7Lr_q7kOJDAd%C5<-EvBfbB`jwm`-UJ(a11Y8
ziH4xF!-cE@l1gESTV}AAEV+>ggHO;~#|YBG`_!7_Qr;YFZ%Lt5>&kiUXxezZ!-
zrZb}g2-9-F;a`}zEeNfxlKCGf0XMYiViMC#n%=E6W{=UW97b2H{H&fDwBnILb&J+b
zg9`YPH^xGy^>z6@Bx_<_y4_s_nGxx4*q~F$&<`hW+9-if;n4UT`hhH6LLw6P&2s3#
zfQ1LTpC==XXShzG0{O@wI;7u?oIAH@ho0w`5Hl>__!9bp|tUB;(imFDd0Z1B=RThcK%ZZE<#bGB)iG?7zNvp0Yn>WfE3{9)ld
z7g}3tP^9Q~L?^YzhL1|hEFt#$^^E&9kc^(?-dBT^@K-Ha??v`XtTOJ)OW!$cs=Huj
z3XH1gBFf)!qI2`u3O|RQ78lZFk#OtwwGWqO{yK2~9h3@yGrlP0F?imi+tc8>ukI
z)baFsQ({4ds#ZdyHr|*%6EYvhp|6=fOLo%n6z0_gymaa3)DM~zmU6Uf;YJpEt&nEk#G
zz0bpR9`fOgir5TpB3#ZN^qlccTnbv1Va49TxczYvGIynz$jMwDfyqs`6}2R&^Z5dF
ze%qw0ZNrBmf6BY${$5|#2!~R!a+g7M9m}4#1t+)Fz%&DwA$S)zeEY=t#U;^8z1!dzqJ=&yL
zB7C%%^dp2oO^0q4BQ=cn^3M6E
z@fs)rOu=1|CJ=e+XEm!KIxOvv#~QViH0XITYnM|MtIZJ(S)@ae=1R68r7lDzEwev2
z8lh)|4XQms?SbSeu0?1Lg^{~V*KNIqm@^cZAJ)@pS)B(e1JV4dSQ9L~piMGk_Sf3J
ze?NJ11(3VM-epxo)gto79nYDd+Sp9M+aol3;c29@VO!vgpd3k{1(YgLo*wC&lBmhS}se%nB3ba
zP7@DS>yG;ce=(GWmJK=9x6r#;Y@nzw_0K
z|HLQ29&12YY75LjJ+#${n5dGN>cS(0OMbM;_l0$?DD72a3d}*VOtcg
ziIjVf0-X77t~&V3F2f@mvhHd&6jDU-il1|p>sAvzlI_cDWNZ(At(fI%@Vl?oD}9`U
z3bFyZF-y7-gBsDkUtu(!=S(8`km1!v<2GCmMk+q3&|fHlb$YA^aRkoFgsqCcRZm0|
z9;s%JDR$aO6M1IbhMQ#xil}48$obUc)!``DAX%Pq)b7;6>fwF&BQ#bcdf+fL;8Qcz
z*~IAbSSPB>X=!rYbzLwzU@PNDX)i887MQAcLATkOz_{(Gx+ZWMAL78O=q6qG!0gHD
zxtkc6wnSY!h}8FF=~b6Gn#J?hbTxOZIO4sG%(rETt0K&AnWkTmyQF9)obYcw4Yn@M
z_PlMx3bbO9hjWN>qxaK|G&psVouv15<#RPiHJva&9;3rXF>Z7>A=B!8iY}GO+*pyX
z9c7s#@#Rnfb=S~uCnP6lBv0^n^TsnYQn9(BeYe>Pp_4tcs45Lqre2!)ZPQW!onrK|
zZ7H*4l91E^j_?AdEkp2Yrd*B@ffW}heFR4{DC~3E_Q#_X^_t^Yq~=wiGil($+VCkM
z(4)vyCnIf685lTws=MOI)T0VzMgNwi%Rr1#))?^(87b;lF>Aas7BqDYGlj%yU;h_P
ztN@gUV&y#W==#p!utoDms
zVS~LF3E5G6{KMCAah7!9GPL=nq`udw;dIoH_JOZ6#;3#yh*eUa#^*>uwTKU=;ega7
z0(o?$Z?mwcX>*6dg(lk{K6Ad@tq)YAN6{fXs()@1*%&-9aG8ZIR6UbpZrTpZ*02AG
z&E)hkyUEjg5%v?ILDs!tx3q$;yJgv_d0CjG+w8v7l}{Ft%1d`Tl$xXnaMdgsStOqq&y-tg?|0DkI>FaTTf}*@FIV#ie&>TJ
zNX3W$dh6F_N0_@lH51H5i@-SigG^M^qPOb@1P@`uS+IWSjCCNaYisoC%Aib&mX_=c
z+8$3?OBbW+Tg#7&dXAuGkbKQmCpN3Qvi&&Tb9w(*`cZ{8!#X&JqWe2gs-bo
zGr=u8f84Y(FsW<84J*1!Zkd3vk?(3^+sT2GjE>kUWhX5CW}UJ=JH|gEU|@sBQHx~m
zO)%DiJuU)Fs~V|jkA!Sk+He`TIq@45qL<0&g1cH2se_x-zfpKxq{zajw$)n1O)mDi
z-S+y92>ULK*iAF>vd+W=&NR%B`7e@?6a*_W0{U%CIJ-t90r~df-btLd>zIKmCWO
zjr>5|J^g+RQ1Y>jiLt(c>6f;Lnj~re@Iqa=|8VoCt-m+qZkSHmb$&XabySlt04N
z(wy9bFi8=>FL{D2BL2kUO%gQ3t3ekSrx?0k>)&(fwIr24LM%&jgZf45U~
z&aaMZ^zz(4GxGbJRulFe-fBk$pkq8u7}*bgQ{9Pr&DjEAnA0jLc~Gbml+$j{aR>|z
zlr=b?#L68Tj`a;|cXWuIw+HNTfH@Kknks(owKXM;zO$?xg65mg0Hy+oaJtEdS>JUK
z`dFneM6GPxLD?Ujk@k4C$qKd4D>LaMeLit4JeHB7JXNte$~2R~XD
zK`%-4G0J1(*@Gxj9vayqkh{_&v80RRrNhy`t6COWQfhAu&_$Yw)^kesl|%Pu6QI=w
zcrj_Q)JJ%$Y&6P^$Kq6cev$q*vN;v{0Z*aOAzVMPMjp02LR_~5Wg_lt16nxn#{Tv%q#M+FNR+y
zdq9gF5q7NfA6n|XZodLbQL!~=1>#Oq{G5nY-Ez-pB0uSfReai7yvu6VYX#!waoQUnU^~W}DCLkjyUmep5J)>{V7x#0mNar`USX0}sowhCu&U0XkWk-6Vbvr}
zD1MUdCyy`DyD&4Btj`E{EU&Qd3a^kh2uMU#(fCeqEmuD9);)g6tt*x?6>iNv$XZ)%
z!b3L-+Y^5hm)V=<-fK&k?P1i4BTp)I5HZC`mc3{lwW7O2nm#bmWTYU~J>EkRkQ1%d
z&G7~_oj
zqF^D%7FHM<3CSYPhem?1prvQgFY@jCWT~2!aU)Oxc=A*0AknNOfc-r8R$QOI
z8V6YM%}+C9Wka#c>yAN@KQG^5#^UQ$F{=$YLdEYe2O7_rgpCYb5v-YVMBiK(H@ld2
z(;@%Fvz5fbcH^1;S2fxuYbk-9|Ha6gVL0+&NE5De@`7xc9{}Le@nZNBZ>QXQcq|VqV4Ha0{8Z9&HS=Jg@nQH)nY(9U{j;g5P
zug*8d#Nozx89O)z-Sm-AioSDFN1ZlBmw~rLZ3e@JrLfaem7$-G#>}QRXO~o9;E5v!
ztkX>i?vo4f^2Y4Z21=g9$E&Lls@W;=zpQsekL5$~-KB#WCbWwZ~auFZ2J8p
z0O1cUan7pSle`xpb1wH@O|ns=W|7&S&?hrKr=Bdah4eKWHX1YzqQ5u~?=K);B`Eu8J&l
z^n26W;^(77hOg5~
zQsjwRtuq5{F%3Hsw)w*}e!>sS*MotNhSc(LFmt>T4qaiP(tL`CProOmKNYzKl7{iS
z9ABq=Qv$rWp|fqBMZ7ln>rxzGlBBCuZfQC{kB{l2SX>SOw+sv`0L}Ea_rypDM0Q~{
z%pHQ*b5fc{ES(z&l@=Zo_(6%#9wQEOvM=G7bH3^D9fy9*>_)_DOvGK{0iJ9nTREmw
zmY_^J0;h<(Vl3}v@dNcBDS1Lto>%I!?^+q}=hC25t-Kt_Mjiu_ni-%Mp>`;aa1Txf}HuMATl9X@720
z4jvb(>gJ!JX7d0x0xS3%Olg-^*eb%x4cW^zIf&U2V>)8ssppvxtDZv
z`l~oYw?uedXyaWMx4~$KR7ZT>+)gNzI=C7ryi}>$CkftA{8k<3j>yACg#1KpY!RXo
z+-q01zEWuL026SKLIJ3o7#6;rvapV}HyLY$K0Rn86Js-X_+&Nvqqh?3(n5cx3seBA
z;g>@YL@2Uvp0zXGx9hc&Mm
z!8_eB?|VIP6xzqoqbVXSeV$TeBx*!evl8pSq@FQ2AhC)>;{RZerEXnmi50IPb&-cM
zmBXOH2CgdgHONda@_hPML(Li`BY0i~c)72So79gyP>Ddzjvl!6U&tk!wlU(W0u^_b
z%p*|ss>-l$lVMD^U>O86F-hqwy7j*wr*T3pmDhr_z}VI58{-M$&@-xrvnznzAI449
zc=|Zh*)6et!ftE$w|Dy9W8sHzZ_%WTHq;j4te}j)+bRnPmdA41(V7C|SqDx{*k%0^
zhOPSGXQAPhd62*mFEK-*mp&7@T?r2{RpJ{RMhJEu