From 3d665ef7b24ad7bd44d36d771534dfbe74b04076 Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Wed, 11 Nov 2020 16:36:10 -0500 Subject: [PATCH 01/52] I accidentally removed this line in a recent PR (#83201) --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0e0099ff672fca..e58d43e07cff54 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -261,6 +261,7 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib # Elasticsearch UI /src/plugins/dev_tools/ @elastic/es-ui /src/plugins/console/ @elastic/es-ui +/src/plugins/es_ui_shared/ @elastic/es-ui /x-pack/plugins/cross_cluster_replication/ @elastic/es-ui /x-pack/plugins/index_lifecycle_management/ @elastic/es-ui /x-pack/plugins/console_extensions/ @elastic/es-ui From 5e06f43adfdecfe719815624a1f659ace83b7047 Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 11 Nov 2020 14:51:10 -0700 Subject: [PATCH 02/52] [kbnClient] retry updating config if necessary (#83205) Co-authored-by: spalger --- packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts index 6ee2d3bfe59b0c..cf3f8f25476cbf 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts @@ -102,6 +102,7 @@ export class KbnClientUiSettings { body: { changes: updates, }, + retries: 3, }); } From 2f26eaf87c2c3d9ef9efbacb02ee70e6a2f4ef5d Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 11 Nov 2020 15:09:45 -0700 Subject: [PATCH 03/52] [ts/checkTsProjects] produce a more useful error message (#83209) Co-authored-by: spalger --- src/dev/typescript/run_check_ts_projects_cli.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/dev/typescript/run_check_ts_projects_cli.ts b/src/dev/typescript/run_check_ts_projects_cli.ts index a095bef047711e..1d332badcd8aed 100644 --- a/src/dev/typescript/run_check_ts_projects_cli.ts +++ b/src/dev/typescript/run_check_ts_projects_cli.ts @@ -17,7 +17,7 @@ * under the License. */ -import { resolve } from 'path'; +import { resolve, relative } from 'path'; import execa from 'execa'; @@ -35,7 +35,7 @@ export async function runCheckTsProjectsCli() { }); const isNotInTsProject: File[] = []; - const isInMultipleTsProjects: File[] = []; + const isInMultipleTsProjects: string[] = []; for (const lineRaw of files.split('\n')) { const line = lineRaw.trim(); @@ -56,7 +56,11 @@ export async function runCheckTsProjectsCli() { isNotInTsProject.push(file); } if (projects.length > 1 && !file.isTypescriptAmbient()) { - isInMultipleTsProjects.push(file); + isInMultipleTsProjects.push( + ` - ${file.getRelativePath()}:\n${projects + .map((p) => ` - ${relative(process.cwd(), p.tsConfigPath)}`) + .join('\n')}` + ); } } @@ -74,10 +78,9 @@ export async function runCheckTsProjectsCli() { } if (isInMultipleTsProjects.length) { + const details = isInMultipleTsProjects.join('\n'); log.error( - `The following files belong to multiple tsconfig.json files listed in src/dev/typescript/projects.ts\n${isInMultipleTsProjects - .map((file) => ` - ${file.getRelativePath()}`) - .join('\n')}` + `The following files belong to multiple tsconfig.json files listed in src/dev/typescript/projects.ts\n${details}` ); } From bc2da6760832c26725fd6e8bacc4fd50de942448 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Wed, 11 Nov 2020 16:23:34 -0600 Subject: [PATCH 04/52] Move Elasticsearch type definitions out of APM (#83081) ...and into x-pack. Also remove `PromiseReturnType` from APM and use the copy in observability everywhere. All of the additional changes to APM imports are just automatic sorting. This makes doing #77720 a little easier and removes some implicit circular dependencies for #80508. Co-authored-by: Dario Gieselaar --- .../shared/KueryBar/get_bool_filter.ts | 2 +- .../plugins/apm/public/utils/testHelpers.tsx | 20 +++++------ .../apm/scripts/shared/get_es_client.ts | 5 ++- .../alerts/register_error_count_alert_type.ts | 2 +- ...egister_transaction_duration_alert_type.ts | 16 ++++----- ...ister_transaction_error_rate_alert_type.ts | 20 +++++------ .../collect_data_telemetry/index.ts | 2 +- .../collect_data_telemetry/tasks.ts | 2 +- .../lib/errors/distribution/get_buckets.ts | 4 +-- .../errors/distribution/get_distribution.ts | 4 +-- .../apm/server/lib/errors/get_error_group.ts | 4 +-- .../apm/server/lib/errors/get_error_groups.ts | 6 ++-- .../get_environment_ui_filter_es.ts | 2 +- .../convert_ui_filters/get_es_filter.ts | 2 +- .../add_filter_to_exclude_legacy_data.ts | 2 +- .../create_apm_event_client/index.ts | 2 +- .../unpack_processor_events.ts | 2 +- .../create_internal_es_client/index.ts | 2 +- .../get_internal_saved_objects_client.ts | 2 +- .../apm/server/lib/helpers/setup_request.ts | 18 +++++----- .../lib/helpers/transaction_error_rate.ts | 2 +- .../metrics/fetch_and_transform_metrics.ts | 12 +++---- .../lib/metrics/transform_metrics_chart.ts | 4 +-- .../lib/service_map/get_service_anomalies.ts | 18 +++++----- .../server/lib/service_map/get_service_map.ts | 14 ++++---- .../get_service_map_service_node_info.ts | 26 +++++++------- .../lib/service_map/get_trace_sample_ids.ts | 16 ++++----- .../get_derived_service_annotations.ts | 8 ++--- .../annotations/get_stored_annotations.ts | 8 ++--- .../lib/services/annotations/index.test.ts | 2 +- .../get_services/get_services_items.ts | 10 +++--- .../server/lib/services/get_services/index.ts | 6 ++-- .../convert_settings_to_string.ts | 2 +- .../find_exact_configuration.ts | 6 ++-- .../search_configurations.ts | 2 +- .../get_correlations_for_slow_transactions.ts | 6 ++-- .../get_duration_for_percentile.ts | 2 +- .../correlations/get_significant_terms_agg.ts | 2 +- .../server/lib/transaction_groups/fetcher.ts | 14 ++++---- .../get_transaction_group_stats.ts | 2 +- .../charts/get_anomaly_data/fetcher.ts | 2 +- .../charts/get_timeseries_data/fetcher.ts | 10 +++--- .../distribution/get_buckets/index.ts | 8 ++--- .../server/lib/ui_filters/get_environments.ts | 8 ++--- .../plugins/apm/server/projections/typings.ts | 4 +-- .../util/merge_projection/index.ts | 10 +++--- .../plugins/apm/server/utils/test_helpers.tsx | 8 ++--- x-pack/plugins/apm/typings/common.d.ts | 6 ---- .../plugins/lens/server/routes/field_stats.ts | 2 +- x-pack/plugins/lens/server/usage/task.ts | 2 +- .../monitoring/workload_statistics.test.ts | 3 +- .../server/monitoring/workload_statistics.ts | 2 +- .../plugins/task_manager/server/task_store.ts | 2 +- .../basic/tests/correlations/ranges.ts | 6 ++-- .../tests/correlations/slow_durations.ts | 4 +-- .../basic/tests/services/top_services.ts | 2 +- .../transaction_groups/transaction_charts.ts | 2 +- .../common/authentication.ts | 2 +- .../test/apm_api_integration/common/config.ts | 2 +- .../trial/tests/service_maps/service_maps.ts | 2 +- .../trial/tests/services/top_services.ts | 2 +- .../services/transaction_groups_charts.ts | 2 +- .../trial/tests/annotations.ts | 2 +- .../elasticsearch/aggregations.d.ts} | 35 +++++++------------ .../elasticsearch/index.d.ts} | 24 +++++++------ 65 files changed, 211 insertions(+), 222 deletions(-) rename x-pack/{plugins/apm/typings/elasticsearch/aggregations.ts => typings/elasticsearch/aggregations.d.ts} (91%) rename x-pack/{plugins/apm/typings/elasticsearch/index.ts => typings/elasticsearch/index.d.ts} (76%) diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts b/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts index f0d12fd16bf7a0..e7dd03db6b63cf 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ESFilter } from '../../../../../../typings/elasticsearch'; import { ERROR_GROUP_ID, PROCESSOR_EVENT, @@ -12,7 +13,6 @@ import { TRANSACTION_TYPE, } from '../../../../common/elasticsearch_fieldnames'; import { UIProcessorEvent } from '../../../../common/processor_event'; -import { ESFilter } from '../../../../typings/elasticsearch'; import { IUrlParams } from '../../../context/UrlParamsContext/types'; export function getBoolFilter({ diff --git a/x-pack/plugins/apm/public/utils/testHelpers.tsx b/x-pack/plugins/apm/public/utils/testHelpers.tsx index f990c4387ddf12..87dfeb95b63263 100644 --- a/x-pack/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/plugins/apm/public/utils/testHelpers.tsx @@ -6,26 +6,26 @@ /* global jest */ -import React from 'react'; -import { ReactWrapper, mount, MountRendererProps } from 'enzyme'; +import { render, waitFor } from '@testing-library/react'; +import { mount, MountRendererProps, ReactWrapper } from 'enzyme'; import enzymeToJson from 'enzyme-to-json'; import { Location } from 'history'; import moment from 'moment'; import { Moment } from 'moment-timezone'; -import { render, waitFor } from '@testing-library/react'; +import React from 'react'; import { MemoryRouter } from 'react-router-dom'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { APMConfig } from '../../server'; -import { PromiseReturnType } from '../../typings/common'; -import { EuiThemeProvider } from '../../../observability/public'; import { ESFilter, - ESSearchResponse, ESSearchRequest, -} from '../../typings/elasticsearch'; + ESSearchResponse, +} from '../../../../typings/elasticsearch'; +import { EuiThemeProvider } from '../../../observability/public'; +import { PromiseReturnType } from '../../../observability/typings/common'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { APMConfig } from '../../server'; +import { UIFilters } from '../../typings/ui_filters'; import { MockApmPluginContextWrapper } from '../context/ApmPluginContext/MockApmPluginContext'; import { UrlParamsProvider } from '../context/UrlParamsContext'; -import { UIFilters } from '../../typings/ui_filters'; const originalConsoleWarn = console.warn; // eslint-disable-line no-console /** diff --git a/x-pack/plugins/apm/scripts/shared/get_es_client.ts b/x-pack/plugins/apm/scripts/shared/get_es_client.ts index 86dfd92190fdf1..912c8943b1edfc 100644 --- a/x-pack/plugins/apm/scripts/shared/get_es_client.ts +++ b/x-pack/plugins/apm/scripts/shared/get_es_client.ts @@ -6,7 +6,10 @@ import { Client } from '@elastic/elasticsearch'; import { ApiKeyAuth, BasicAuth } from '@elastic/elasticsearch/lib/pool'; -import { ESSearchResponse, ESSearchRequest } from '../../typings/elasticsearch'; +import { + ESSearchResponse, + ESSearchRequest, +} from '../../../../typings/elasticsearch'; export type ESClient = ReturnType; diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts index ecda5b0e8504bf..464a737c50ea2a 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts @@ -9,6 +9,7 @@ import { isEmpty } from 'lodash'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; import { APMConfig } from '../..'; +import { ESSearchResponse } from '../../../../../typings/elasticsearch'; import { AlertingPlugin } from '../../../../alerts/server'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; import { @@ -17,7 +18,6 @@ import { SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; -import { ESSearchResponse } from '../../../typings/elasticsearch'; import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from './action_variables'; diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts index d9e69c8f3b7d7c..602ee99970f8ae 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts @@ -7,21 +7,21 @@ import { schema } from '@kbn/config-schema'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; -import { getDurationFormatter } from '../../../common/utils/formatters'; -import { ProcessorEvent } from '../../../common/processor_event'; +import { APMConfig } from '../..'; +import { ESSearchResponse } from '../../../../../typings/elasticsearch'; +import { AlertingPlugin } from '../../../../alerts/server'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; -import { ESSearchResponse } from '../../../typings/elasticsearch'; import { PROCESSOR_EVENT, + SERVICE_ENVIRONMENT, SERVICE_NAME, - TRANSACTION_TYPE, TRANSACTION_DURATION, - SERVICE_ENVIRONMENT, + TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; -import { AlertingPlugin } from '../../../../alerts/server'; -import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; -import { APMConfig } from '../..'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { getDurationFormatter } from '../../../common/utils/formatters'; import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from './action_variables'; interface RegisterAlertParams { diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts index 06b296db5a4853..0506e1b4c3aed6 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts @@ -5,25 +5,25 @@ */ import { schema } from '@kbn/config-schema'; +import { isEmpty } from 'lodash'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; -import { isEmpty } from 'lodash'; -import { asDecimalOrInteger } from '../../../common/utils/formatters'; -import { ProcessorEvent } from '../../../common/processor_event'; -import { EventOutcome } from '../../../common/event_outcome'; +import { APMConfig } from '../..'; +import { ESSearchResponse } from '../../../../../typings/elasticsearch'; +import { AlertingPlugin } from '../../../../alerts/server'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; -import { ESSearchResponse } from '../../../typings/elasticsearch'; import { + EVENT_OUTCOME, PROCESSOR_EVENT, + SERVICE_ENVIRONMENT, SERVICE_NAME, TRANSACTION_TYPE, - EVENT_OUTCOME, - SERVICE_ENVIRONMENT, } from '../../../common/elasticsearch_fieldnames'; -import { AlertingPlugin } from '../../../../alerts/server'; -import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; -import { APMConfig } from '../..'; +import { EventOutcome } from '../../../common/event_outcome'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { asDecimalOrInteger } from '../../../common/utils/formatters'; import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from './action_variables'; interface RegisterAlertParams { diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts index e53a0e24b4723d..730645c609cb60 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts @@ -9,7 +9,7 @@ import { IndicesStatsParams, Client } from 'elasticsearch'; import { ESSearchRequest, ESSearchResponse, -} from '../../../../typings/elasticsearch'; +} from '../../../../../../typings/elasticsearch'; import { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; import { tasks } from './tasks'; import { APMDataTelemetry } from '../types'; diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts index ac82d353417b49..ec2eb7c8dd99a4 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts @@ -5,7 +5,7 @@ */ import { ValuesType } from 'utility-types'; import { flatten, merge, sortBy, sum, pickBy } from 'lodash'; -import { AggregationOptionsByType } from '../../../../typings/elasticsearch/aggregations'; +import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch/aggregations'; import { ProcessorEvent } from '../../../../common/processor_event'; import { TelemetryTask } from '.'; import { AGENT_NAMES, RUM_AGENT_NAMES } from '../../../../common/agent_name'; diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts index b12dd73a20986b..9e18c81e4aaeb4 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ProcessorEvent } from '../../../../common/processor_event'; -import { ESFilter } from '../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; import { ERROR_GROUP_ID, SERVICE_NAME, } from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; import { rangeFilter } from '../../../../common/utils/range_filter'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts index dea518cad8e405..7866c993534518 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PromiseReturnType } from '../../../../typings/common'; +import { PromiseReturnType } from '../../../../../observability/typings/common'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { getBuckets } from './get_buckets'; import { BUCKET_TARGET_COUNT } from '../../transactions/constants'; +import { getBuckets } from './get_buckets'; function getBucketSize({ start, end }: SetupTimeRange) { return Math.floor((end - start) / BUCKET_TARGET_COUNT); diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_group.ts b/x-pack/plugins/apm/server/lib/errors/get_error_group.ts index 0fbc7720f7111f..37be72beedeb1c 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_group.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_group.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ProcessorEvent } from '../../../common/processor_event'; +import { PromiseReturnType } from '../../../../observability/typings/common'; import { ERROR_GROUP_ID, SERVICE_NAME, TRANSACTION_SAMPLED, } from '../../../common/elasticsearch_fieldnames'; -import { PromiseReturnType } from '../../../typings/common'; +import { ProcessorEvent } from '../../../common/processor_event'; import { rangeFilter } from '../../../common/utils/range_filter'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getTransaction } from '../transactions/get_transaction'; diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts index 006d2fae3d4fb1..d734a1395fc5e4 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SortOptions } from '../../../../../typings/elasticsearch/aggregations'; +import { PromiseReturnType } from '../../../../observability/typings/common'; import { ERROR_CULPRIT, ERROR_EXC_HANDLED, @@ -12,11 +14,9 @@ import { ERROR_GROUP_ID, ERROR_LOG_MESSAGE, } from '../../../common/elasticsearch_fieldnames'; -import { PromiseReturnType } from '../../../typings/common'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getErrorGroupsProjection } from '../../projections/errors'; import { mergeProjection } from '../../projections/util/merge_projection'; -import { SortOptions } from '../../../typings/elasticsearch/aggregations'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; export type ErrorGroupListAPIResponse = PromiseReturnType< typeof getErrorGroups 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 ea8d02eb833cfd..dd9f0b0333c013 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESFilter } from '../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; import { ENVIRONMENT_NOT_DEFINED, ENVIRONMENT_ALL, diff --git a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_es_filter.ts b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_es_filter.ts index 1b8f32d4de8b93..0592f13bdf54ff 100644 --- a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_es_filter.ts +++ b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_es_filter.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESFilter } from '../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; import { UIFilters } from '../../../../typings/ui_filters'; import { getEnvironmentUiFilterES } from './get_environment_ui_filter_es'; import { diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts index 494cd6cbf0eec0..921b46daee2a9d 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts @@ -9,7 +9,7 @@ import { OBSERVER_VERSION_MAJOR } from '../../../../../common/elasticsearch_fiel import { ESSearchRequest, ESFilter, -} from '../../../../../typings/elasticsearch'; +} from '../../../../../../../typings/elasticsearch'; /* Adds a range query to the ES request to exclude legacy data diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts index d23d729db39241..870997efb77dee 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts @@ -14,7 +14,7 @@ import { ProcessorEvent } from '../../../../../common/processor_event'; import { ESSearchRequest, ESSearchResponse, -} from '../../../../../typings/elasticsearch'; +} from '../../../../../../../typings/elasticsearch'; import { ApmIndicesConfig } from '../../../settings/apm_indices/get_apm_indices'; import { addFilterToExcludeLegacyData } from './add_filter_to_exclude_legacy_data'; import { callClientWithDebug } from '../call_client_with_debug'; diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts index 736c7ad2d1089f..add522a5c7f84f 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts @@ -10,7 +10,7 @@ import { ProcessorEvent } from '../../../../../common/processor_event'; import { ESSearchRequest, ESFilter, -} from '../../../../../typings/elasticsearch'; +} from '../../../../../../../typings/elasticsearch'; import { APMEventESSearchRequest } from '.'; import { ApmIndicesConfig, diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts index 072391606d574e..8e74a7992e9ea7 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts @@ -15,7 +15,7 @@ import { APMRequestHandlerContext } from '../../../../routes/typings'; import { ESSearchResponse, ESSearchRequest, -} from '../../../../../typings/elasticsearch'; +} from '../../../../../../../typings/elasticsearch'; import { callClientWithDebug } from '../call_client_with_debug'; // `type` was deprecated in 7.0 diff --git a/x-pack/plugins/apm/server/lib/helpers/get_internal_saved_objects_client.ts b/x-pack/plugins/apm/server/lib/helpers/get_internal_saved_objects_client.ts index e0bb9ad354f588..097538076de257 100644 --- a/x-pack/plugins/apm/server/lib/helpers/get_internal_saved_objects_client.ts +++ b/x-pack/plugins/apm/server/lib/helpers/get_internal_saved_objects_client.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { CoreSetup } from 'src/core/server'; -import { PromiseReturnType } from '../../../typings/common'; +import { PromiseReturnType } from '../../../../observability/typings/common'; export type InternalSavedObjectsClient = PromiseReturnType< typeof getInternalSavedObjectsClient diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index 363c4128137e02..65d36c8b36af88 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -4,20 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import moment from 'moment'; import { Logger } from 'kibana/server'; -import { isActivePlatinumLicense } from '../../../common/service_map'; -import { UI_SETTINGS } from '../../../../../../src/plugins/data/common'; -import { KibanaRequest } from '../../../../../../src/core/server'; +import moment from 'moment'; import { APMConfig } from '../..'; +import { KibanaRequest } from '../../../../../../src/core/server'; +import { UI_SETTINGS } from '../../../../../../src/plugins/data/common'; +import { ESFilter } from '../../../../../typings/elasticsearch'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { isActivePlatinumLicense } from '../../../common/service_map'; +import { UIFilters } from '../../../typings/ui_filters'; +import { APMRequestHandlerContext } from '../../routes/typings'; import { - getApmIndices, ApmIndicesConfig, + getApmIndices, } from '../settings/apm_indices/get_apm_indices'; -import { ESFilter } from '../../../typings/elasticsearch'; import { getEsFilter } from './convert_ui_filters/get_es_filter'; -import { APMRequestHandlerContext } from '../../routes/typings'; -import { ProcessorEvent } from '../../../common/processor_event'; import { APMEventClient, createApmEventClient, @@ -26,7 +27,6 @@ import { APMInternalClient, createInternalESClient, } from './create_es_client/create_internal_es_client'; -import { UIFilters } from '../../../typings/ui_filters'; // Explicitly type Setup to prevent TS initialization errors // https://github.com/microsoft/TypeScript/issues/34933 diff --git a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts index ccc7ba7e22b504..03a44e77ba2d3d 100644 --- a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts @@ -9,7 +9,7 @@ import { EventOutcome } from '../../../common/event_outcome'; import { AggregationOptionsByType, AggregationResultOf, -} from '../../../typings/elasticsearch/aggregations'; +} from '../../../../../typings/elasticsearch/aggregations'; import { getTransactionDurationFieldForAggregatedTransactions } from './aggregated_transactions'; export function getOutcomeAggregation({ diff --git a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts index 3ccba8c7586dcf..57eae26593305b 100644 --- a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Unionize, Overwrite } from 'utility-types'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { getMetricsDateHistogramParams } from '../helpers/metrics'; -import { ChartBase } from './types'; -import { transformDataToMetricsChart } from './transform_metrics_chart'; +import { Overwrite, Unionize } from 'utility-types'; +import { AggregationOptionsByType } from '../../../../../typings/elasticsearch'; import { getMetricsProjection } from '../../projections/metrics'; import { mergeProjection } from '../../projections/util/merge_projection'; -import { AggregationOptionsByType } from '../../../typings/elasticsearch/aggregations'; import { APMEventESSearchRequest } from '../helpers/create_es_client/create_apm_event_client'; +import { getMetricsDateHistogramParams } from '../helpers/metrics'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { transformDataToMetricsChart } from './transform_metrics_chart'; +import { ChartBase } from './types'; type MetricsAggregationMap = Unionize<{ min: AggregationOptionsByType['min']; diff --git a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts index a191d5400e36cf..bcf8b412f350a0 100644 --- a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts +++ b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { ChartBase } from './types'; -import { ESSearchResponse } from '../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../typings/elasticsearch'; import { getVizColorForIndex } from '../../../common/viz_colors'; import { GenericMetricsRequest } from './fetch_and_transform_metrics'; +import { ChartBase } from './types'; export type GenericMetricsChart = ReturnType< typeof transformDataToMetricsChart diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts index 5f4bc61af4c69d..8f8d7763970b7b 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -4,21 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ import Boom from '@hapi/boom'; +import { MlPluginSetup } from '../../../../ml/server'; +import { PromiseReturnType } from '../../../../observability/typings/common'; +import { + getSeverity, + ML_ERRORS, + ServiceAnomalyStats, +} from '../../../common/anomaly_detection'; +import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; import { getServiceHealthStatus } from '../../../common/service_health_status'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { PromiseReturnType } from '../../../typings/common'; import { TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST, } from '../../../common/transaction_types'; -import { - ServiceAnomalyStats, - getSeverity, - ML_ERRORS, -} from '../../../common/anomaly_detection'; import { getMlJobsWithAPMGroup } from '../anomaly_detection/get_ml_jobs_with_apm_group'; -import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; -import { MlPluginSetup } from '../../../../ml/server'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; export const DEFAULT_ANOMALIES = { mlJobIds: [], serviceAnomalies: {} }; diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index 330bb936c9e889..ccebbfa44538a1 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -3,8 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { chunk } from 'lodash'; import { Logger } from 'kibana/server'; +import { chunk } from 'lodash'; +import { PromiseReturnType } from '../../../../observability/typings/common'; import { AGENT_NAME, SERVICE_ENVIRONMENT, @@ -12,17 +13,16 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { getServicesProjection } from '../../projections/services'; import { mergeProjection } from '../../projections/util/merge_projection'; -import { PromiseReturnType } from '../../../typings/common'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; -import { transformServiceMapResponses } from './transform_service_map_responses'; -import { getServiceMapFromTraceIds } from './get_service_map_from_trace_ids'; -import { getTraceSampleIds } from './get_trace_sample_ids'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { + DEFAULT_ANOMALIES, getServiceAnomalies, ServiceAnomaliesResponse, - DEFAULT_ANOMALIES, } from './get_service_anomalies'; +import { getServiceMapFromTraceIds } from './get_service_map_from_trace_ids'; +import { getTraceSampleIds } from './get_trace_sample_ids'; +import { transformServiceMapResponses } from './transform_service_map_responses'; export interface IEnvOptions { setup: Setup & SetupTimeRange; 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 37b34641435fbc..82d339686f7ec2 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 @@ -4,33 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ESFilter } from '../../../../../typings/elasticsearch'; import { - TRANSACTION_REQUEST, - TRANSACTION_PAGE_LOAD, -} from '../../../common/transaction_types'; -import { - SERVICE_NAME, + METRIC_CGROUP_MEMORY_USAGE_BYTES, METRIC_SYSTEM_CPU_PERCENT, METRIC_SYSTEM_FREE_MEMORY, METRIC_SYSTEM_TOTAL_MEMORY, - METRIC_CGROUP_MEMORY_USAGE_BYTES, + SERVICE_NAME, TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; +import { + TRANSACTION_PAGE_LOAD, + TRANSACTION_REQUEST, +} from '../../../common/transaction_types'; import { rangeFilter } from '../../../common/utils/range_filter'; -import { ESFilter } from '../../../typings/elasticsearch'; +import { + getDocumentTypeFilterForAggregatedTransactions, + getProcessorEventForAggregatedTransactions, + getTransactionDurationFieldForAggregatedTransactions, +} from '../helpers/aggregated_transactions'; +import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { percentCgroupMemoryUsedScript, percentSystemMemoryUsedScript, } from '../metrics/by_agent/shared/memory'; -import { - getProcessorEventForAggregatedTransactions, - getTransactionDurationFieldForAggregatedTransactions, - getDocumentTypeFilterForAggregatedTransactions, -} from '../helpers/aggregated_transactions'; import { getErrorRate } from '../transaction_groups/get_error_rate'; -import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; interface Options { setup: Setup & SetupTimeRange; diff --git a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts index 22c655bb96f503..db11ab2d9ce3a5 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts @@ -3,20 +3,20 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { uniq, take, sortBy } from 'lodash'; import Boom from '@hapi/boom'; -import { ProcessorEvent } from '../../../common/processor_event'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { rangeFilter } from '../../../common/utils/range_filter'; -import { ESFilter } from '../../../typings/elasticsearch'; +import { sortBy, take, uniq } from 'lodash'; +import { ESFilter } from '../../../../../typings/elasticsearch'; import { - SERVICE_NAME, SERVICE_ENVIRONMENT, - TRACE_ID, + SERVICE_NAME, SPAN_DESTINATION_SERVICE_RESOURCE, + TRACE_ID, } from '../../../common/elasticsearch_fieldnames'; -import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { ProcessorEvent } from '../../../common/processor_event'; import { SERVICE_MAP_TIMEOUT_ERROR } from '../../../common/service_map'; +import { rangeFilter } from '../../../common/utils/range_filter'; +import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; const MAX_TRACES_TO_INSPECT = 1000; 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 b80e86d53f2922..367cf2faf6a8e0 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 @@ -4,19 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ import { isNumber } from 'lodash'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; import { Annotation, AnnotationType } from '../../../../common/annotations'; -import { SetupTimeRange, Setup } from '../../helpers/setup_request'; -import { ESFilter } from '../../../../typings/elasticsearch'; -import { rangeFilter } from '../../../../common/utils/range_filter'; import { SERVICE_NAME, SERVICE_VERSION, } from '../../../../common/elasticsearch_fieldnames'; -import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { rangeFilter } from '../../../../common/utils/range_filter'; import { getDocumentTypeFilterForAggregatedTransactions, getProcessorEventForAggregatedTransactions, } from '../../helpers/aggregated_transactions'; +import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; export async function getDerivedServiceAnnotations({ setup, 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 6e3ae0181ddeef..623abf69302970 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 @@ -5,13 +5,13 @@ */ import { LegacyAPICaller, Logger } from 'kibana/server'; -import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; -import { ESSearchResponse } from '../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../../typings/elasticsearch'; +import { Annotation as ESAnnotation } from '../../../../../observability/common/annotations'; import { ScopedAnnotationsClient } from '../../../../../observability/server'; import { Annotation, AnnotationType } from '../../../../common/annotations'; -import { Annotation as ESAnnotation } from '../../../../../observability/common/annotations'; -import { SetupTimeRange, Setup } from '../../helpers/setup_request'; +import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; export async function getStoredAnnotations({ setup, diff --git a/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts b/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts index f30b77f1477100..16c46be78cb284 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts @@ -6,7 +6,7 @@ import { ESSearchRequest, ESSearchResponse, -} from '../../../../typings/elasticsearch'; +} from '../../../../../../typings/elasticsearch'; import { inspectSearchParams, SearchParamsMock, diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index 5ea3714e81b6fe..89915e798b7cd2 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ import { Logger } from '@kbn/logging'; +import { PromiseReturnType } from '../../../../../observability/typings/common'; import { joinByKey } from '../../../../common/utils/join_by_key'; -import { PromiseReturnType } from '../../../../typings/common'; -import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { getServicesProjection } from '../../../projections/services'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { - getTransactionDurationAverages, getAgentNames, - getTransactionRates, - getTransactionErrorRates, getEnvironments, getHealthStatuses, + getTransactionDurationAverages, + getTransactionErrorRates, + getTransactionRates, } from './get_services_items_stats'; export type ServiceListAPIResponse = PromiseReturnType; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/index.ts b/x-pack/plugins/apm/server/lib/services/get_services/index.ts index 5f39d6c836930d..9d450804e421d0 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/index.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/index.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEmpty } from 'lodash'; import { Logger } from '@kbn/logging'; -import { PromiseReturnType } from '../../../../typings/common'; +import { isEmpty } from 'lodash'; +import { PromiseReturnType } from '../../../../../observability/typings/common'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { hasHistoricalAgentData } from './has_historical_agent_data'; import { getLegacyDataStatus } from './get_legacy_data_status'; import { getServicesItems } from './get_services_items'; +import { hasHistoricalAgentData } from './has_historical_agent_data'; export type ServiceListAPIResponse = PromiseReturnType; diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/convert_settings_to_string.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/convert_settings_to_string.ts index ab01a68733a7a9..6791bf85a7b6ac 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/convert_settings_to_string.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/convert_settings_to_string.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESSearchHit } from '../../../../typings/elasticsearch'; +import { ESSearchHit } from '../../../../../../typings/elasticsearch'; import { AgentConfiguration } from '../../../../common/agent_configuration/configuration_types'; // needed for backwards compatability diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts index 3a9623c7775166..bd10df76b85c77 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ESSearchHit } from '../../../../../../typings/elasticsearch'; +import { AgentConfiguration } from '../../../../common/agent_configuration/configuration_types'; import { - SERVICE_NAME, SERVICE_ENVIRONMENT, + SERVICE_NAME, } from '../../../../common/elasticsearch_fieldnames'; import { Setup } from '../../helpers/setup_request'; -import { AgentConfiguration } from '../../../../common/agent_configuration/configuration_types'; -import { ESSearchHit } from '../../../../typings/elasticsearch'; import { convertConfigSettingsToString } from './convert_settings_to_string'; export async function findExactConfiguration({ diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts index 2585f88c2aa431..43ae4995ce672b 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts @@ -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 { ESSearchHit } from '../../../../typings/elasticsearch'; +import { ESSearchHit } from '../../../../../../typings/elasticsearch'; import { SERVICE_NAME, SERVICE_ENVIRONMENT, diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_correlations_for_slow_transactions.ts b/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_correlations_for_slow_transactions.ts index 3efc65afdfd280..76e595c928cf2c 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_correlations_for_slow_transactions.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_correlations_for_slow_transactions.ts @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { asDuration } from '../../../../common/utils/formatters'; -import { ESFilter } from '../../../../typings/elasticsearch'; -import { rangeFilter } from '../../../../common/utils/range_filter'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; import { SERVICE_NAME, TRANSACTION_DURATION, @@ -14,6 +12,8 @@ import { TRANSACTION_TYPE, } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; +import { asDuration } from '../../../../common/utils/formatters'; +import { rangeFilter } from '../../../../common/utils/range_filter'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { getDurationForPercentile } from './get_duration_for_percentile'; import { diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_duration_for_percentile.ts b/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_duration_for_percentile.ts index 37ee19ff40f62a..a94540df10fffb 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_duration_for_percentile.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_duration_for_percentile.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESFilter } from '../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_significant_terms_agg.ts b/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_significant_terms_agg.ts index 1cf0787c1d9704..c5ab8d8f1d111e 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_significant_terms_agg.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/correlations/get_significant_terms_agg.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESFilter } from '../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; import { SignificantTermsScoring } from './scoring_rt'; export function getSignificantTermsAgg({ diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index 0a4d9748f25977..89fff260a7d231 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -3,24 +3,24 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { take, sortBy } from 'lodash'; -import { Unionize } from 'utility-types'; +import { sortBy, take } from 'lodash'; import moment from 'moment'; -import { joinByKey } from '../../../common/utils/join_by_key'; +import { Unionize } from 'utility-types'; +import { AggregationOptionsByType } from '../../../../../typings/elasticsearch'; +import { PromiseReturnType } from '../../../../observability/typings/common'; import { SERVICE_NAME, TRANSACTION_NAME, } from '../../../common/elasticsearch_fieldnames'; +import { joinByKey } from '../../../common/utils/join_by_key'; import { getTransactionGroupsProjection } from '../../projections/transaction_groups'; import { mergeProjection } from '../../projections/util/merge_projection'; -import { PromiseReturnType } from '../../../../observability/typings/common'; -import { AggregationOptionsByType } from '../../../typings/elasticsearch/aggregations'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getAverages, - getSums, - getPercentiles, getCounts, + getPercentiles, + getSums, } from './get_transaction_group_stats'; interface TopTransactionOptions { diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts index 2550bd70c527d3..cfd3540446172b 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts @@ -6,7 +6,7 @@ import { merge } from 'lodash'; import { TRANSACTION_TYPE } from '../../../common/elasticsearch_fieldnames'; import { arrayUnionToCallable } from '../../../common/utils/array_union_to_callable'; -import { AggregationInputMap } from '../../../typings/elasticsearch/aggregations'; +import { AggregationInputMap } from '../../../../../typings/elasticsearch'; import { TransactionGroupRequestBase, TransactionGroupSetup } from './fetcher'; import { getTransactionDurationFieldForAggregatedTransactions } from '../helpers/aggregated_transactions'; 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 index 8c999b445d7995..aad67c43f48e20 100644 --- 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 @@ -5,7 +5,7 @@ */ import { Logger } from 'kibana/server'; -import { ESSearchResponse } from '../../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../../../typings/elasticsearch'; import { PromiseReturnType } from '../../../../../../observability/typings/common'; import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts index e2edbbec63d475..a2da3977b81c76 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts @@ -4,22 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESFilter } from '../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { PromiseReturnType } from '../../../../../../observability/typings/common'; import { SERVICE_NAME, TRANSACTION_NAME, TRANSACTION_RESULT, TRANSACTION_TYPE, } from '../../../../../common/elasticsearch_fieldnames'; -import { PromiseReturnType } from '../../../../../../observability/typings/common'; -import { getBucketSize } from '../../../helpers/get_bucket_size'; import { rangeFilter } from '../../../../../common/utils/range_filter'; -import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; import { + getDocumentTypeFilterForAggregatedTransactions, getProcessorEventForAggregatedTransactions, getTransactionDurationFieldForAggregatedTransactions, - getDocumentTypeFilterForAggregatedTransactions, } from '../../../helpers/aggregated_transactions'; +import { getBucketSize } from '../../../helpers/get_bucket_size'; +import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; export type ESResponse = PromiseReturnType; export function timeseriesFetcher({ diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts index 34d01627a2869d..010acd09239a32 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { ValuesType } from 'utility-types'; -import { PromiseReturnType } from '../../../../../typings/common'; -import { joinByKey } from '../../../../../common/utils/join_by_key'; -import { ProcessorEvent } from '../../../../../common/processor_event'; +import { PromiseReturnType } from '../../../../../../observability/typings/common'; import { SERVICE_NAME, TRACE_ID, @@ -16,13 +14,15 @@ import { TRANSACTION_SAMPLED, TRANSACTION_TYPE, } from '../../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../../common/processor_event'; +import { joinByKey } from '../../../../../common/utils/join_by_key'; import { rangeFilter } from '../../../../../common/utils/range_filter'; -import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; import { getDocumentTypeFilterForAggregatedTransactions, getProcessorEventForAggregatedTransactions, getTransactionDurationFieldForAggregatedTransactions, } from '../../../helpers/aggregated_transactions'; +import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; function getHistogramAggOptions({ bucketSize, diff --git a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts index b9f25e20f9f730..8109becbed45e3 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ProcessorEvent } from '../../../common/processor_event'; +import { ESFilter } from '../../../../../typings/elasticsearch'; import { SERVICE_ENVIRONMENT, SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../../../common/utils/range_filter'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; -import { ESFilter } from '../../../typings/elasticsearch'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { rangeFilter } from '../../../common/utils/range_filter'; import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; export async function getEnvironments({ setup, diff --git a/x-pack/plugins/apm/server/projections/typings.ts b/x-pack/plugins/apm/server/projections/typings.ts index 332ac533e78c61..b6d6ffbb7ddbfc 100644 --- a/x-pack/plugins/apm/server/projections/typings.ts +++ b/x-pack/plugins/apm/server/projections/typings.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESSearchBody } from '../../typings/elasticsearch'; import { AggregationOptionsByType, AggregationInputMap, -} from '../../typings/elasticsearch/aggregations'; + ESSearchBody, +} from '../../../../typings/elasticsearch'; import { APMEventESSearchRequest } from '../lib/helpers/create_es_client/create_apm_event_client'; export type Projection = Omit & { diff --git a/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts b/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts index ea7267dd337c25..2782b039958e1e 100644 --- a/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts +++ b/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts @@ -3,12 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { mergeWith, isPlainObject, cloneDeep } from 'lodash'; +import { cloneDeep, isPlainObject, mergeWith } from 'lodash'; import { DeepPartial } from 'utility-types'; -import { AggregationInputMap } from '../../../../typings/elasticsearch/aggregations'; -import { ESSearchBody } from '../../../../typings/elasticsearch'; -import { Projection } from '../../typings'; +import { + AggregationInputMap, + ESSearchBody, +} from '../../../../../../typings/elasticsearch'; import { APMEventESSearchRequest } from '../../../lib/helpers/create_es_client/create_apm_event_client'; +import { Projection } from '../../typings'; type PlainObject = Record; diff --git a/x-pack/plugins/apm/server/utils/test_helpers.tsx b/x-pack/plugins/apm/server/utils/test_helpers.tsx index 21b59dc516d060..9093b16fada0d5 100644 --- a/x-pack/plugins/apm/server/utils/test_helpers.tsx +++ b/x-pack/plugins/apm/server/utils/test_helpers.tsx @@ -3,14 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { APMConfig } from '../'; +import { PromiseReturnType } from '../../../observability/typings/common'; import { ESFilter, - ESSearchResponse, ESSearchRequest, -} from '../../typings/elasticsearch'; -import { PromiseReturnType } from '../../typings/common'; + ESSearchResponse, +} from '../../../../typings/elasticsearch'; import { UIFilters } from '../../typings/ui_filters'; -import { APMConfig } from '..'; interface Options { mockResponse?: ( diff --git a/x-pack/plugins/apm/typings/common.d.ts b/x-pack/plugins/apm/typings/common.d.ts index 754529a1985528..9133315c4c16a5 100644 --- a/x-pack/plugins/apm/typings/common.d.ts +++ b/x-pack/plugins/apm/typings/common.d.ts @@ -25,10 +25,4 @@ export type PromiseValueType = Value extends Promise ? Value : Value; -export type PromiseReturnType = Func extends ( - ...args: any[] -) => Promise - ? Value - : Func; - export type Maybe = T | null | undefined; diff --git a/x-pack/plugins/lens/server/routes/field_stats.ts b/x-pack/plugins/lens/server/routes/field_stats.ts index a884aeffa6134b..29e2416b74618b 100644 --- a/x-pack/plugins/lens/server/routes/field_stats.ts +++ b/x-pack/plugins/lens/server/routes/field_stats.ts @@ -9,7 +9,7 @@ import DateMath from '@elastic/datemath'; import { schema } from '@kbn/config-schema'; import { CoreSetup } from 'src/core/server'; import { IFieldType } from 'src/plugins/data/common'; -import { ESSearchResponse } from '../../../apm/typings/elasticsearch'; +import { ESSearchResponse } from '../../../../typings/elasticsearch'; import { FieldStatsResponse, BASE_API_URL } from '../../common'; const SHARD_SIZE = 5000; diff --git a/x-pack/plugins/lens/server/usage/task.ts b/x-pack/plugins/lens/server/usage/task.ts index 83cdbd62f3484b..014193fb6566ea 100644 --- a/x-pack/plugins/lens/server/usage/task.ts +++ b/x-pack/plugins/lens/server/usage/task.ts @@ -15,7 +15,7 @@ import { } from '../../../task_manager/server'; import { getVisualizationCounts } from './visualization_counts'; -import { ESSearchResponse } from '../../../apm/typings/elasticsearch'; +import { ESSearchResponse } from '../../../../typings/elasticsearch'; // This task is responsible for running daily and aggregating all the Lens click event objects // into daily rolled-up documents, which will be used in reporting click stats diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts index cb6e48530b027a..d1c5256c81c63d 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts @@ -13,8 +13,7 @@ import { estimateRecurringTaskScheduling, } from './workload_statistics'; import { ConcreteTaskInstance } from '../task'; -import { ESSearchResponse } from '../../../apm/typings/elasticsearch'; -import { AggregationResultOf } from '../../../apm/typings/elasticsearch/aggregations'; +import { AggregationResultOf, ESSearchResponse } from '../../../../typings/elasticsearch'; import { times } from 'lodash'; import { taskStoreMock } from '../task_store.mock'; import { of, Subject } from 'rxjs'; diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts index 17448ea412ae63..a27b5e2282e329 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts @@ -11,7 +11,7 @@ import { JsonObject } from 'src/plugins/kibana_utils/common'; import { keyBy, mapValues } from 'lodash'; import { AggregatedStatProvider } from './runtime_statistics_aggregator'; import { parseIntervalAsSecond, asInterval, parseIntervalAsMillisecond } from '../lib/intervals'; -import { AggregationResultOf } from '../../../apm/typings/elasticsearch/aggregations'; +import { AggregationResultOf } from '../../../../typings/elasticsearch'; import { HealthStatus } from './monitoring_stats_stream'; import { TaskStore } from '../task_store'; diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index 8c0d7764e009f8..cb44144d2fefe6 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -56,7 +56,7 @@ import { } from './queries/mark_available_tasks_as_claimed'; import { TaskTypeDictionary } from './task_type_dictionary'; -import { ESSearchResponse, ESSearchBody } from '../../apm/typings/elasticsearch'; +import { ESSearchResponse, ESSearchBody } from '../../../typings/elasticsearch'; export interface StoreOpts { esClient: ElasticsearchClient; diff --git a/x-pack/test/apm_api_integration/basic/tests/correlations/ranges.ts b/x-pack/test/apm_api_integration/basic/tests/correlations/ranges.ts index f013520fa163b9..0a730217e53f57 100644 --- a/x-pack/test/apm_api_integration/basic/tests/correlations/ranges.ts +++ b/x-pack/test/apm_api_integration/basic/tests/correlations/ranges.ts @@ -6,10 +6,10 @@ import expect from '@kbn/expect'; import { format } from 'url'; -import { expectSnapshot } from '../../../common/match_snapshot'; -import { PromiseReturnType } from '../../../../../plugins/apm/typings/common'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; import archives_metadata from '../../../common/archives_metadata'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { expectSnapshot } from '../../../common/match_snapshot'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/apm_api_integration/basic/tests/correlations/slow_durations.ts b/x-pack/test/apm_api_integration/basic/tests/correlations/slow_durations.ts index 085a81c5f1bf4b..0cfdf3ec474d5a 100644 --- a/x-pack/test/apm_api_integration/basic/tests/correlations/slow_durations.ts +++ b/x-pack/test/apm_api_integration/basic/tests/correlations/slow_durations.ts @@ -6,10 +6,10 @@ import expect from '@kbn/expect'; import { format } from 'url'; -import { PromiseReturnType } from '../../../../../plugins/apm/typings/common'; +import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; +import archives_metadata from '../../../common/archives_metadata'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { expectSnapshot } from '../../../common/match_snapshot'; -import archives_metadata from '../../../common/archives_metadata'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts index b4bbec1e0f861f..cd2bdb7fde19e8 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { isEmpty, pick } from 'lodash'; -import { PromiseReturnType } from '../../../../../plugins/apm/typings/common'; +import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import archives_metadata from '../../../common/archives_metadata'; diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts index d7d6d613281ef7..c3b969d7656642 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; import archives_metadata from '../../../common/archives_metadata'; -import { PromiseReturnType } from '../../../../../plugins/apm/typings/common'; +import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; diff --git a/x-pack/test/apm_api_integration/common/authentication.ts b/x-pack/test/apm_api_integration/common/authentication.ts index 46401224f0d8c9..3d1e03aa032f08 100644 --- a/x-pack/test/apm_api_integration/common/authentication.ts +++ b/x-pack/test/apm_api_integration/common/authentication.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PromiseReturnType } from '../../../plugins/apm/typings/common'; +import { PromiseReturnType } from '../../../plugins/observability/typings/common'; import { SecurityServiceProvider } from '../../../../test/common/services/security'; type SecurityService = PromiseReturnType; diff --git a/x-pack/test/apm_api_integration/common/config.ts b/x-pack/test/apm_api_integration/common/config.ts index db073cb9674230..f94bd6bd3be6f9 100644 --- a/x-pack/test/apm_api_integration/common/config.ts +++ b/x-pack/test/apm_api_integration/common/config.ts @@ -9,7 +9,7 @@ import supertestAsPromised from 'supertest-as-promised'; import { format, UrlObject } from 'url'; import path from 'path'; import { InheritedFtrProviderContext, InheritedServices } from './ftr_provider_context'; -import { PromiseReturnType } from '../../../plugins/apm/typings/common'; +import { PromiseReturnType } from '../../../plugins/observability/typings/common'; import { createApmUser, APM_TEST_PASSWORD, ApmUser } from './authentication'; interface Settings { diff --git a/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts b/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts index 0cd91eb46a5e25..25c3f753341d16 100644 --- a/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts +++ b/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts @@ -8,7 +8,7 @@ import querystring from 'querystring'; import expect from '@kbn/expect'; import { isEmpty, uniq } from 'lodash'; import archives_metadata from '../../../common/archives_metadata'; -import { PromiseReturnType } from '../../../../../plugins/apm/typings/common'; +import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; diff --git a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts index 9a6c6f94dbb60e..bb611013351d77 100644 --- a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { expectSnapshot } from '../../../common/match_snapshot'; -import { PromiseReturnType } from '../../../../../plugins/apm/typings/common'; +import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import archives_metadata from '../../../common/archives_metadata'; diff --git a/x-pack/test/apm_api_integration/trial/tests/services/transaction_groups_charts.ts b/x-pack/test/apm_api_integration/trial/tests/services/transaction_groups_charts.ts index c35dfcc3817a42..47e465596e0d72 100644 --- a/x-pack/test/apm_api_integration/trial/tests/services/transaction_groups_charts.ts +++ b/x-pack/test/apm_api_integration/trial/tests/services/transaction_groups_charts.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { expectSnapshot } from '../../../common/match_snapshot'; -import { PromiseReturnType } from '../../../../../plugins/apm/typings/common'; +import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import archives_metadata from '../../../common/archives_metadata'; diff --git a/x-pack/test/observability_api_integration/trial/tests/annotations.ts b/x-pack/test/observability_api_integration/trial/tests/annotations.ts index f47a0f75cbfde7..32e41c2c463e1c 100644 --- a/x-pack/test/observability_api_integration/trial/tests/annotations.ts +++ b/x-pack/test/observability_api_integration/trial/tests/annotations.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { JsonObject } from 'src/plugins/kibana_utils/common'; import { Annotation } from '../../../../plugins/observability/common/annotations'; -import { ESSearchHit } from '../../../../plugins/apm/typings/elasticsearch'; +import { ESSearchHit } from '../../../../typings/elasticsearch'; import { FtrProviderContext } from '../../common/ftr_provider_context'; const DEFAULT_INDEX_NAME = 'observability-annotations'; diff --git a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts b/x-pack/typings/elasticsearch/aggregations.d.ts similarity index 91% rename from x-pack/plugins/apm/typings/elasticsearch/aggregations.ts rename to x-pack/typings/elasticsearch/aggregations.d.ts index a869c1e1186fa0..29c78e93831751 100644 --- a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts +++ b/x-pack/typings/elasticsearch/aggregations.d.ts @@ -202,10 +202,7 @@ type SubAggregationResponseOf< ? AggregationResponseMap : {}; -interface AggregationResponsePart< - TAggregationOptionsMap extends AggregationOptionsMap, - TDocument -> { +interface AggregationResponsePart { terms: { buckets: Array< { @@ -224,8 +221,7 @@ interface AggregationResponsePart< }; date_histogram: { buckets: Array< - DateHistogramBucket & - SubAggregationResponseOf + DateHistogramBucket & SubAggregationResponseOf >; }; avg: MetricsAggregationResponsePart; @@ -277,10 +273,7 @@ interface AggregationResponsePart< } & SubAggregationResponseOf; filters: TAggregationOptionsMap extends { filters: { filters: any[] } } ? Array< - { doc_count: number } & AggregationResponseMap< - TAggregationOptionsMap['aggs'], - TDocument - > + { doc_count: number } & AggregationResponseMap > : TAggregationOptionsMap extends { filters: { @@ -291,10 +284,7 @@ interface AggregationResponsePart< buckets: { [key in keyof TAggregationOptionsMap['filters']['filters']]: { doc_count: number; - } & SubAggregationResponseOf< - TAggregationOptionsMap['aggs'], - TDocument - >; + } & SubAggregationResponseOf; }; } : never; @@ -337,18 +327,15 @@ interface AggregationResponsePart< buckets: TAggregationOptionsMap extends { range: { keyed: true } } ? Record< string, - DateRangeBucket & - SubAggregationResponseOf + DateRangeBucket & SubAggregationResponseOf > : Array< - DateRangeBucket & - SubAggregationResponseOf + DateRangeBucket & SubAggregationResponseOf >; }; auto_date_histogram: { buckets: Array< - DateHistogramBucket & - AggregationResponseMap + DateHistogramBucket & AggregationResponseMap >; interval: string; }; @@ -389,9 +376,11 @@ interface AggregationResponsePart< // Union keys are not included in keyof. The type will fall back to keyof T if // UnionToIntersection fails, which happens when there are conflicts between the union // types, e.g. { foo: string; bar?: undefined } | { foo?: undefined; bar: string }; -export type ValidAggregationKeysOf< - T extends Record -> = keyof (UnionToIntersection extends never ? T : UnionToIntersection); +export type ValidAggregationKeysOf> = keyof (UnionToIntersection< + T +> extends never + ? T + : UnionToIntersection); export type AggregationResultOf< TAggregationOptionsMap extends AggregationOptionsMap, diff --git a/x-pack/plugins/apm/typings/elasticsearch/index.ts b/x-pack/typings/elasticsearch/index.d.ts similarity index 76% rename from x-pack/plugins/apm/typings/elasticsearch/index.ts rename to x-pack/typings/elasticsearch/index.d.ts index 9a05fe631e888d..f5d595edcd71e2 100644 --- a/x-pack/plugins/apm/typings/elasticsearch/index.ts +++ b/x-pack/typings/elasticsearch/index.d.ts @@ -6,6 +6,17 @@ import { SearchParams, SearchResponse } from 'elasticsearch'; import { AggregationResponseMap, AggregationInputMap } from './aggregations'; +export { + AggregationInputMap, + AggregationOptionsByType, + AggregationResponseMap, + AggregationResultOf, + SortOptions, + ValidAggregationKeysOf, +} from './aggregations'; + +// Typings for Elasticsearch queries and aggregations. These are intended to be +// moved to the Elasticsearch JS client at some point (see #77720.) interface CollapseQuery { field: string; @@ -42,10 +53,7 @@ export type ESSearchResponse< > = Omit, 'aggregations' | 'hits'> & (TSearchRequest extends { body: { aggs: AggregationInputMap } } ? { - aggregations?: AggregationResponseMap< - TSearchRequest['body']['aggs'], - TDocument - >; + aggregations?: AggregationResponseMap; } : {}) & { hits: Omit['hits'], 'total'> & @@ -63,12 +71,6 @@ export type ESSearchResponse< export interface ESFilter { [key: string]: { - [key: string]: - | string - | string[] - | number - | boolean - | Record - | ESFilter[]; + [key: string]: string | string[] | number | boolean | Record | ESFilter[]; }; } From c27422fdc8abf1d65332d2762135081b7a8629b0 Mon Sep 17 00:00:00 2001 From: Constance Date: Wed, 11 Nov 2020 15:43:47 -0800 Subject: [PATCH 05/52] Add enzyme rerender test helper (#83208) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__mocks__/enzyme_rerender.mock.ts | 16 ++++++++++++++++ .../public/applications/__mocks__/index.ts | 1 + .../form_components/key_engine_access.test.tsx | 3 ++- .../components/engine/engine_nav.test.tsx | 5 +++-- .../enterprise_search/index.test.tsx | 3 ++- 5 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/__mocks__/enzyme_rerender.mock.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/enzyme_rerender.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/enzyme_rerender.mock.ts new file mode 100644 index 00000000000000..1508f27c003777 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/enzyme_rerender.mock.ts @@ -0,0 +1,16 @@ +/* + * 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 { ShallowWrapper } from 'enzyme'; + +/** + * Quick and easy helper for re-rendering a React component in Enzyme + * after (e.g.) updating Kea values + */ +export const rerender = (wrapper: ShallowWrapper) => { + wrapper.setProps({}); // Re-renders + wrapper.update(); // Just in case +}; 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 bdf555311d154e..e8944b15978a9f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts @@ -15,4 +15,5 @@ export { mockAllValues, mockAllActions, setMockValues, setMockActions } from './ export { mountAsync } from './mount_async.mock'; export { mountWithIntl } from './mount_with_i18n.mock'; export { shallowWithIntl } from './shallow_with_i18n.mock'; +export { rerender } from './enzyme_rerender.mock'; // Note: shallow_useeffect must be imported directly as a file diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.test.tsx index b4b092f17a6aa9..d0bbf868aa90c7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.test.tsx @@ -5,6 +5,7 @@ */ import { setMockValues, setMockActions } from '../../../../../__mocks__/kea.mock'; +import { rerender } from '../../../../../__mocks__'; import React from 'react'; import { shallow } from 'enzyme'; @@ -60,7 +61,7 @@ describe('FormKeyEngineAccess', () => { ...values, fullEngineAccessChecked: false, }); - wrapper.setProps({}); // Re-render + rerender(wrapper); expect(wrapper.find('#all_engines').prop('checked')).toEqual(false); expect(wrapper.find('#all_engines').prop('value')).toEqual('false'); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx index 7e8228a89cb8d7..0d2ce654d4a0ab 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx @@ -5,6 +5,7 @@ */ import { setMockValues } from '../../../__mocks__/kea.mock'; +import { rerender } from '../../../__mocks__'; import React from 'react'; import { shallow } from 'enzyme'; @@ -40,11 +41,11 @@ describe('EngineNav', () => { expect(wrapper.find(EuiBadge)).toHaveLength(0); setMockValues({ ...values, isSampleEngine: true }); - wrapper.setProps({}); // Re-render + rerender(wrapper); expect(wrapper.find(EuiBadge).prop('children')).toEqual('SAMPLE ENGINE'); setMockValues({ ...values, isMetaEngine: true }); - wrapper.setProps({}); // Re-render + rerender(wrapper); expect(wrapper.find(EuiBadge).prop('children')).toEqual('META ENGINE'); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx index 0e929c9191e0f5..7f638c64b445d8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { setMockValues } from '../__mocks__/kea.mock'; +import { rerender } from '../__mocks__'; import { EnterpriseSearch } from './'; import { SetupGuide } from './components/setup_guide'; @@ -40,7 +41,7 @@ describe('EnterpriseSearch', () => { errorConnecting: true, config: { host: '' }, }); - wrapper.setProps({}); // Re-render + rerender(wrapper); expect(wrapper.find(ErrorConnecting)).toHaveLength(0); expect(wrapper.find(ProductSelector)).toHaveLength(1); From 8bd2809bb7e14ea0ffb75e58e12f61f6bbfc6b6e Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 11 Nov 2020 16:59:25 -0700 Subject: [PATCH 06/52] skip flaky suite (#83231) --- x-pack/test/functional/apps/logstash/pipeline_create.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/logstash/pipeline_create.js b/x-pack/test/functional/apps/logstash/pipeline_create.js index 1fd09ce393785c..c3cf102c908a0c 100644 --- a/x-pack/test/functional/apps/logstash/pipeline_create.js +++ b/x-pack/test/functional/apps/logstash/pipeline_create.js @@ -15,7 +15,8 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['logstash']); const retry = getService('retry'); - describe('pipeline create new', () => { + // FLAKY: https://github.com/elastic/kibana/issues/83231 + describe.skip('pipeline create new', () => { let originalWindowSize; before(async () => { From 5a9c456088305d3a8fed5642ebafa439a70983a4 Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Wed, 11 Nov 2020 16:05:27 -0800 Subject: [PATCH 07/52] Trying to fix CODEOWERS, missing a starting slash (#83233) --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e58d43e07cff54..7ca24b76c9636c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -124,7 +124,7 @@ /x-pack/plugins/transform/ @elastic/ml-ui /x-pack/test/functional/apps/transform/ @elastic/ml-ui /x-pack/test/functional/services/transform/ @elastic/ml-ui/ -x-pack/test/api_integration_basic/apis/ml/ @elastic/ml-ui +/x-pack/test/api_integration_basic/apis/ml/ @elastic/ml-ui /x-pack/test/functional_basic/apps/ml/ @elastic/ml-ui /x-pack/test/api_integration_basic/apis/transform/ @elastic/ml-ui From 52c2416d9d06a7f9d3505583e6f76b041fd11b05 Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Wed, 11 Nov 2020 16:16:04 -0800 Subject: [PATCH 08/52] Trying to fix CODEOWNERS again, where was a non-existent team prior (#83236) --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7ca24b76c9636c..4c8d74d6baee69 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -26,7 +26,7 @@ /src/plugins/vis_type_vislib/ @elastic/kibana-app /src/plugins/vis_type_xy/ @elastic/kibana-app /src/plugins/visualize/ @elastic/kibana-app -/src/plugins/visualizations/ @elastic/kibana-application +/src/plugins/visualizations/ @elastic/kibana-app # Application Services /examples/bfetch_explorer/ @elastic/kibana-app-arch From 6f1fa748973f76ebcca16cf74ced871b04614273 Mon Sep 17 00:00:00 2001 From: kobelb Date: Wed, 11 Nov 2020 16:54:46 -0800 Subject: [PATCH 09/52] Removing unnecessary trailing slash in CODEOWNERS --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4c8d74d6baee69..62abf281e659f3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -123,7 +123,7 @@ # ML team owns and maintains the transform plugin despite it living in the Elasticsearch management section. /x-pack/plugins/transform/ @elastic/ml-ui /x-pack/test/functional/apps/transform/ @elastic/ml-ui -/x-pack/test/functional/services/transform/ @elastic/ml-ui/ +/x-pack/test/functional/services/transform/ @elastic/ml-ui /x-pack/test/api_integration_basic/apis/ml/ @elastic/ml-ui /x-pack/test/functional_basic/apps/ml/ @elastic/ml-ui From b44a0e5b02992d174099a53d9028309c57fb1942 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Thu, 12 Nov 2020 05:45:17 +0100 Subject: [PATCH 10/52] [Discover] Unskip flaky tests based on discover fixture index pattern (#82991) --- .../apps/discover/_doc_navigation.js | 10 ++--- test/functional/apps/discover/_field_data.js | 3 +- test/functional/apps/discover/_inspector.js | 3 +- .../fixtures/es_archiver/discover/data.json | 42 ++++++++++++++++++ .../es_archiver/discover/data.json.gz | Bin 1120 -> 0 bytes 5 files changed, 47 insertions(+), 11 deletions(-) create mode 100644 test/functional/fixtures/es_archiver/discover/data.json delete mode 100644 test/functional/fixtures/es_archiver/discover/data.json.gz diff --git a/test/functional/apps/discover/_doc_navigation.js b/test/functional/apps/discover/_doc_navigation.js index 31aef96918ffab..59c4371b95d807 100644 --- a/test/functional/apps/discover/_doc_navigation.js +++ b/test/functional/apps/discover/_doc_navigation.js @@ -20,7 +20,6 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { - const log = getService('log'); const docTable = getService('docTable'); const filterBar = getService('filterBar'); const testSubjects = getService('testSubjects'); @@ -28,15 +27,12 @@ export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const retry = getService('retry'); - // FLAKY: https://github.com/elastic/kibana/issues/78373 - describe.skip('doc link in discover', function contextSize() { + describe('doc link in discover', function contextSize() { beforeEach(async function () { - log.debug('load kibana index with default index pattern'); - await esArchiver.loadIfNeeded('discover'); - await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.loadIfNeeded('discover'); + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await PageObjects.common.navigateToApp('discover'); - await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.discover.waitForDocTableLoadingComplete(); }); diff --git a/test/functional/apps/discover/_field_data.js b/test/functional/apps/discover/_field_data.js index 118234d54626c7..d45b8f4841cb6a 100644 --- a/test/functional/apps/discover/_field_data.js +++ b/test/functional/apps/discover/_field_data.js @@ -27,8 +27,7 @@ export default function ({ getService, getPageObjects }) { const queryBar = getService('queryBar'); const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']); - // FLAKY: https://github.com/elastic/kibana/issues/78689 - describe.skip('discover tab', function describeIndexTests() { + describe('discover tab', function describeIndexTests() { this.tags('includeFirefox'); before(async function () { await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/test/functional/apps/discover/_inspector.js b/test/functional/apps/discover/_inspector.js index fcb66fbd52cf73..900ad28e14e691 100644 --- a/test/functional/apps/discover/_inspector.js +++ b/test/functional/apps/discover/_inspector.js @@ -34,8 +34,7 @@ export default function ({ getService, getPageObjects }) { return hitsCountStatsRow[STATS_ROW_VALUE_INDEX]; } - // FLAKY: https://github.com/elastic/kibana/issues/39842 - describe.skip('inspect', () => { + describe('inspect', () => { before(async () => { await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('discover'); diff --git a/test/functional/fixtures/es_archiver/discover/data.json b/test/functional/fixtures/es_archiver/discover/data.json new file mode 100644 index 00000000000000..9158a3023fc5e3 --- /dev/null +++ b/test/functional/fixtures/es_archiver/discover/data.json @@ -0,0 +1,42 @@ +{ + "type": "doc", + "value": { + "id": "index-pattern:logstash-*", + "index": ".kibana", + "source": { + "index-pattern": { + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@message\"}}},{\"name\":\"@tags\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@tags\"}}},{\"name\":\"@timestamp\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"agent\"}}},{\"name\":\"bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"extension\"}}},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"headings\"}}},{\"name\":\"host\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"host\"}}},{\"name\":\"id\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"index\"}}},{\"name\":\"ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"links\"}}},{\"name\":\"machine.os\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"machine.os\"}}},{\"name\":\"machine.ram\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"esTypes\":[\"double\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nestedField.child\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"nested\":{\"path\":\"nestedField\"}}},{\"name\":\"phpmemory\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:section\"}}},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:tag\"}}},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:description\"}}},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image\"}}},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:height\"}}},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:width\"}}},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:site_name\"}}},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:title\"}}},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:type\"}}},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:url\"}}},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:card\"}}},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:description\"}}},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:image\"}}},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:site\"}}},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:title\"}}},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.url\"}}},{\"name\":\"request\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"request\"}}},{\"name\":\"response\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"response\"}}},{\"name\":\"spaces\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"spaces\"}}},{\"name\":\"type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"url\"}}},{\"name\":\"utc_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"xss\"}}}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "type": "index-pattern" + } + } +} + +{ + "type": "doc", + "value": { + "id": "search:ab12e3c0-f231-11e6-9486-733b1ac9221a", + "index": ".kibana", + "source": { + "search": { + "columns": [ + "_source" + ], + "description": "A Saved Search Description", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\n \"index\": \"logstash-*\",\n \"highlightAll\": true,\n \"filter\": [],\n \"query\": {\n \"query_string\": {\n \"query\": \"*\",\n \"analyze_wildcard\": true\n }\n }\n}" + }, + "sort": [ + "@timestamp", + "desc" + ], + "title": "A Saved Search", + "version": 1 + }, + "type": "search" + } + } +} diff --git a/test/functional/fixtures/es_archiver/discover/data.json.gz b/test/functional/fixtures/es_archiver/discover/data.json.gz deleted file mode 100644 index 047d890f6d410178d6c0c385c8bdde46f804bc33..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1120 zcmV-m1fTmKiwFo&46QOZ*Bn1TVHSEHVl97r!f3DG(po2w_Tok*szBI zyL;%~E`_1UOst)3`6Ri27Wli5l5EFLQ>^7-C2^P_OXU^nWVYm z>A54^G1)lE4lnRhAB#*OlIw@NEk!YW;^QVko>pjYYn%A@r}W3{NZp2k&EStMTIf5Ww?E#!E) zVbTDSyg*N}x^V=ACm;Ww+=DNl`S&*6ZSDCO0o#I;@b>aM%0#?>ZYp1cWH{(As*p}VFC$030iAhreN%A!#36J=iN8jKgB0-U!c zVIGj8`eq2VAacT&zs7V&y;`Y@x`a~c$9NV4}OZ;D_{ zd@+Qc@^5N*c{z-HM*FxzxsG&05NiP*&?bhu$V9xB%-(1z@OE=#9L3^`t#Q;QpN19M zC6ucIa-}rJe8hDS^^uv?N_58(Ctt6iW{3lguPSw@;&GbZqph%#J|R6horwaKsa0jD zVO*DB4E>^^FMz3tR=fvA^>~1Vh)is;2SaHpU#cXZ!AIi?ai%ny6Cat;`{uCtjwY&6 zM|oG=CO%$?Z?+ak)2|6wi^oE@u`hELHDce_%stk zko?WdUpHvu5eEJ%8h16{-zBO$l^s(Nwcv?QA3w_as<$f<)H)|(6V8Kmuc zG~s{ETs!v^;wo+1_O|P%_iggdgFWjrzLD#e-{+6%ZJ6^Nd(^Gpaohg3q5FT@mG8Oj znftY8XSHkS8~O_~h3Scl1yptdnvvA_ctu z;V{!-*3Krb`p21DjC{EmiTvXA?J3ES>Oghx=p|*DcwE6a~x+Q zq;-=8-}p9AzBk}byHN1{GV8r~LJiUeA)UKs_c#Yak7y8t%r^Ymy$0LSn;WI=p~{as mVW;imb{HIVYJsuV??9Virp2?a+quQWXa50HZ?gK@G5`QD?ly%0 From 268bd095438751ec1f2e92585868927f450d68cd Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Thu, 12 Nov 2020 09:09:28 +0100 Subject: [PATCH 11/52] [Lens] Remove background from lens embeddable (#83061) --- .../pie_visualization/render_function.tsx | 7 ++- .../__snapshots__/expression.test.tsx.snap | 63 ++++++++++++++++--- .../public/xy_visualization/expression.tsx | 53 +++++++--------- 3 files changed, 85 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index eec351cfbb27ea..39743a355fd78b 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -253,7 +253,12 @@ export function PieComponent( onClickValue(desanitizeFilterContext(context)); }} - theme={chartTheme} + theme={{ + ...chartTheme, + background: { + color: undefined, // removes background for embeddables + }, + }} baseTheme={chartBaseTheme} /> , - valuesLabelMode: string = 'hide', - isHorizontal: boolean -) { +function getValueLabelsStyling(isHorizontal: boolean) { const VALUE_LABELS_MAX_FONTSIZE = 15; const VALUE_LABELS_MIN_FONTSIZE = 10; const VALUE_LABELS_VERTICAL_OFFSET = -10; const VALUE_LABELS_HORIZONTAL_OFFSET = 10; - if (valuesLabelMode === 'hide') { - return theme; - } return { - ...theme, - ...{ - barSeriesStyle: { - ...theme.barSeriesStyle, - displayValue: { - fontSize: { min: VALUE_LABELS_MIN_FONTSIZE, max: VALUE_LABELS_MAX_FONTSIZE }, - fill: { textInverted: true, textBorder: 2 }, - alignment: isHorizontal - ? { - vertical: VerticalAlignment.Middle, - } - : { horizontal: HorizontalAlignment.Center }, - offsetX: isHorizontal ? VALUE_LABELS_HORIZONTAL_OFFSET : 0, - offsetY: isHorizontal ? 0 : VALUE_LABELS_VERTICAL_OFFSET, - }, - }, + displayValue: { + fontSize: { min: VALUE_LABELS_MIN_FONTSIZE, max: VALUE_LABELS_MAX_FONTSIZE }, + fill: { textInverted: true, textBorder: 2 }, + alignment: isHorizontal + ? { + vertical: VerticalAlignment.Middle, + } + : { horizontal: HorizontalAlignment.Center }, + offsetX: isHorizontal ? VALUE_LABELS_HORIZONTAL_OFFSET : 0, + offsetY: isHorizontal ? 0 : VALUE_LABELS_VERTICAL_OFFSET, }, }; } @@ -445,9 +430,8 @@ export function XYChart({ // No histogram charts !isHistogramViz; - const baseThemeWithMaybeValueLabels = !shouldShowValueLabels - ? chartTheme - : mergeThemeWithValueLabelsStyling(chartTheme, valueLabels, shouldRotate); + const valueLabelsStyling = + shouldShowValueLabels && valueLabels !== 'hide' && getValueLabelsStyling(shouldRotate); const colorAssignments = getColorAssignments(args.layers, data, formatFactory); @@ -461,7 +445,16 @@ export function XYChart({ } legendPosition={legend.position} showLegendExtra={false} - theme={baseThemeWithMaybeValueLabels} + theme={{ + ...chartTheme, + barSeriesStyle: { + ...chartTheme.barSeriesStyle, + ...valueLabelsStyling, + }, + background: { + color: undefined, // removes background for embeddables + }, + }} baseTheme={chartBaseTheme} tooltip={{ headerFormatter: (d) => safeXAccessorLabelRenderer(d.value), From 5a91818baac7ac4312415654d972886da90ffa39 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Thu, 12 Nov 2020 10:11:48 +0100 Subject: [PATCH 12/52] Remove unused asciidoc file (#83228) --- docs/settings/ssl-settings.asciidoc | 99 ----------------------------- 1 file changed, 99 deletions(-) delete mode 100644 docs/settings/ssl-settings.asciidoc diff --git a/docs/settings/ssl-settings.asciidoc b/docs/settings/ssl-settings.asciidoc deleted file mode 100644 index 3a0a474d9d597b..00000000000000 --- a/docs/settings/ssl-settings.asciidoc +++ /dev/null @@ -1,99 +0,0 @@ -[float] -=== {component} TLS/SSL settings -You can configure the following TLS/SSL settings. If the settings are not -configured, the default values are used. See -{ref}/security-settings.html[Default TLS/SSL Settings]. - -ifdef::server[] -+{ssl-prefix}.ssl.enabled+:: -Used to enable or disable TLS/SSL. The default is `false`. -endif::server[] - -+{ssl-prefix}.ssl.supported_protocols+:: -Supported protocols with versions. Valid protocols: `SSLv2Hello`, -`SSLv3`, `TLSv1`, `TLSv1.1`, `TLSv1.2`. Defaults to `TLSv1.2`, `TLSv1.1`, -`TLSv1`. Defaults to the value of `xpack.ssl.supported_protocols`. - -ifdef::server[] -+{ssl-prefix}.ssl.client_authentication+:: -Controls the server's behavior in regard to requesting a certificate -from client connections. Valid values are `required`, `optional`, and `none`. -`required` forces a client to present a certificate, while `optional` -requests a client certificate but the client is not required to present one. -ifndef::client-auth-default[] -Defaults to the value of `xpack.ssl.client_authentication`. -endif::client-auth-default[] -ifdef::client-auth-default[] -Defaults to +{client-auth-default}+. -endif::client-auth-default[] -endif::server[] - -ifdef::verifies[] -+{ssl-prefix}.ssl.verification_mode+:: -Controls the verification of certificates. Valid values are `none`, -`certificate`, and `full`. Defaults to the value of `xpack.ssl.verification_mode`. -endif::verifies[] - -+{ssl-prefix}.ssl.cipher_suites+:: -Supported cipher suites can be found in Oracle's http://docs.oracle.com/javase/8/docs/technotes/guides/security/SunProviders.html[ -Java Cryptography Architecture documentation]. Defaults to the value of -`xpack.ssl.cipher_suites`. - -[float] -==== {component} TLS/SSL key and trusted certificate settings - -The following settings are used to specify a private key, certificate, and the -trusted certificates that should be used when communicating over an SSL/TLS connection. -If none of the settings are specified, the default values are used. -See {ref}/security-settings.html[Default TLS/SSL settings]. - -ifdef::server[] -A private key and certificate must be configured. -endif::server[] -ifndef::server[] -A private key and certificate are optional and would be used if the server requires client authentication for PKI -authentication. -endif::server[] -If none of the settings bare specified, the defaults values are used. -See {ref}/security-settings.html[Default TLS/SSL settings]. - -[float] -===== PEM encoded files - -When using PEM encoded files, use the following settings: - -+{ssl-prefix}.ssl.key+:: -Path to a PEM encoded file containing the private key. - -+{ssl-prefix}.ssl.key_passphrase+:: -The passphrase that will be used to decrypt the private key. This value is -optional as the key may not be encrypted. - -+{ssl-prefix}.ssl.certificate+:: -Path to a PEM encoded file containing the certificate (or certificate chain) -that will be presented when requested. - -+{ssl-prefix}.ssl.certificate_authorities+:: -List of paths to the PEM encoded certificate files that should be trusted. - -[float] -===== Java keystore files - -When using Java keystore files (JKS), which contain the private key, certificate -and certificates that should be trusted, use the following settings: - -+{ssl-prefix}.ssl.keystore.path+:: -Path to the keystore that holds the private key and certificate. - -+{ssl-prefix}.ssl.keystore.password+:: -Password to the keystore. - -+{ssl-prefix}.ssl.keystore.key_password+:: -Password for the private key in the keystore. Defaults to the -same value as +{ssl-prefix}.ssl.keystore.password+. - -+{ssl-prefix}.ssl.truststore.path+:: -Path to the truststore file. - -+{ssl-prefix}.ssl.truststore.password+:: -Password to the truststore. From 4721b3211a5e674ff61f92017a380a2c2fb60a05 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Thu, 12 Nov 2020 05:47:43 -0500 Subject: [PATCH 13/52] [Ingest Manger] Move asset getters out of registry (#83214) ## Summary Packages/Archives aren't limited to the registry any longer. Continue moving file- & cache-related functions from services/registry to services/archive. Move `getAsset` and `pathParts` to archive/index. The behavior is the same for now, but it's more accurate to separate these from registry namespace. Registry has `fetch*` and other functions for dealing with the online service. --- .../server/services/epm/archive/index.ts | 40 +++++++++++++++- .../services/epm/elasticsearch/ilm/install.ts | 8 ++-- .../elasticsearch/ingest_pipeline/install.ts | 11 +++-- .../epm/elasticsearch/template/install.ts | 14 +++--- .../epm/elasticsearch/transform/common.ts | 6 +-- .../epm/elasticsearch/transform/install.ts | 4 +- .../services/epm/kibana/assets/install.ts | 8 ++-- .../server/services/epm/packages/assets.ts | 4 +- .../services/epm/registry/index.test.ts | 5 +- .../server/services/epm/registry/index.ts | 48 +++---------------- 10 files changed, 75 insertions(+), 73 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/epm/archive/index.ts b/x-pack/plugins/fleet/server/services/epm/archive/index.ts index 28f635e9412ae4..810740d697fcbc 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/index.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ArchivePackage } from '../../../../common/types'; +import { ArchivePackage, AssetParts } from '../../../../common/types'; import { PackageInvalidArchiveError, PackageUnsupportedMediaTypeError } from '../../../errors'; import { + cacheGet, cacheSet, cacheDelete, getArchiveFilelist, @@ -100,3 +101,40 @@ export const deletePackageCache = (name: string, version: string) => { // this has been populated in unpackArchiveToCache() paths?.forEach((path) => cacheDelete(path)); }; + +export function getPathParts(path: string): AssetParts { + let dataset; + + let [pkgkey, service, type, file] = path.split('/'); + + // if it's a data stream + if (service === 'data_stream') { + // save the dataset name + dataset = type; + // drop the `data_stream/dataset-name` portion & re-parse + [pkgkey, service, type, file] = path.replace(`data_stream/${dataset}/`, '').split('/'); + } + + // This is to cover for the fields.yml files inside the "fields" directory + if (file === undefined) { + file = type; + type = 'fields'; + service = ''; + } + + return { + pkgkey, + service, + type, + file, + dataset, + path, + } as AssetParts; +} + +export function getAsset(key: string) { + const buffer = cacheGet(key); + if (buffer === undefined) throw new Error(`Cannot find asset ${key}`); + + return buffer; +} diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ilm/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ilm/install.ts index c5253e4902cabd..46c0729a650d0a 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ilm/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ilm/install.ts @@ -5,15 +5,15 @@ */ import { CallESAsCurrentUser, ElasticsearchAssetType } from '../../../../types'; -import * as Registry from '../../registry'; +import { getAsset, getPathParts } from '../../archive'; export async function installILMPolicy(paths: string[], callCluster: CallESAsCurrentUser) { const ilmPaths = paths.filter((path) => isILMPolicy(path)); if (!ilmPaths.length) return; await Promise.all( ilmPaths.map(async (path) => { - const body = Registry.getAsset(path).toString('utf-8'); - const { file } = Registry.pathParts(path); + const body = getAsset(path).toString('utf-8'); + const { file } = getPathParts(path); const name = file.substr(0, file.lastIndexOf('.')); try { await callCluster('transport.request', { @@ -28,7 +28,7 @@ export async function installILMPolicy(paths: string[], callCluster: CallESAsCur ); } const isILMPolicy = (path: string) => { - const pathParts = Registry.pathParts(path); + const pathParts = getPathParts(path); return pathParts.type === ElasticsearchAssetType.ilmPolicy; }; export async function policyExists( diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts index 58abdeb0d443d5..c5c9e8ac2c01ba 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts @@ -11,7 +11,8 @@ import { ElasticsearchAssetType, InstallablePackage, } from '../../../../types'; -import * as Registry from '../../registry'; +import { ArchiveEntry } from '../../registry'; +import { getAsset, getPathParts } from '../../archive'; import { CallESAsCurrentUser } from '../../../../types'; import { saveInstalledEsRefs } from '../../packages/install'; import { getInstallationObject } from '../../packages'; @@ -127,7 +128,7 @@ export async function installPipelinesForDataStream({ dataStream, packageVersion: pkgVersion, }); - const content = Registry.getAsset(path).toString('utf-8'); + const content = getAsset(path).toString('utf-8'); pipelines.push({ name, nameForInstallation, @@ -192,10 +193,10 @@ async function installPipeline({ return { id: pipeline.nameForInstallation, type: ElasticsearchAssetType.ingestPipeline }; } -const isDirectory = ({ path }: Registry.ArchiveEntry) => path.endsWith('/'); +const isDirectory = ({ path }: ArchiveEntry) => path.endsWith('/'); const isDataStreamPipeline = (path: string, dataStreamDataset: string) => { - const pathParts = Registry.pathParts(path); + const pathParts = getPathParts(path); return ( !isDirectory({ path }) && pathParts.type === ElasticsearchAssetType.ingestPipeline && @@ -204,7 +205,7 @@ const isDataStreamPipeline = (path: string, dataStreamDataset: string) => { ); }; const isPipeline = (path: string) => { - const pathParts = Registry.pathParts(path); + const pathParts = getPathParts(path); return pathParts.type === ElasticsearchAssetType.ingestPipeline; }; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts index 25d412b685904e..199026da30c11f 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts @@ -17,7 +17,7 @@ import { CallESAsCurrentUser } from '../../../../types'; import { Field, loadFieldsFromYaml, processFields } from '../../fields/field'; import { getPipelineNameForInstallation } from '../ingest_pipeline/install'; import { generateMappings, generateTemplateName, getTemplate } from './template'; -import * as Registry from '../../registry'; +import { getAsset, getPathParts } from '../../archive'; import { removeAssetsFromInstalledEsByType, saveInstalledEsRefs } from '../../packages/install'; export const installTemplates = async ( @@ -76,9 +76,9 @@ export const installTemplates = async ( const installPreBuiltTemplates = async (paths: string[], callCluster: CallESAsCurrentUser) => { const templatePaths = paths.filter((path) => isTemplate(path)); const templateInstallPromises = templatePaths.map(async (path) => { - const { file } = Registry.pathParts(path); + const { file } = getPathParts(path); const templateName = file.substr(0, file.lastIndexOf('.')); - const content = JSON.parse(Registry.getAsset(path).toString('utf8')); + const content = JSON.parse(getAsset(path).toString('utf8')); let templateAPIPath = '_template'; // v2 index templates need to be installed through the new API endpoint. @@ -121,9 +121,9 @@ const installPreBuiltComponentTemplates = async ( ) => { const templatePaths = paths.filter((path) => isComponentTemplate(path)); const templateInstallPromises = templatePaths.map(async (path) => { - const { file } = Registry.pathParts(path); + const { file } = getPathParts(path); const templateName = file.substr(0, file.lastIndexOf('.')); - const content = JSON.parse(Registry.getAsset(path).toString('utf8')); + const content = JSON.parse(getAsset(path).toString('utf8')); const callClusterParams: { method: string; @@ -151,12 +151,12 @@ const installPreBuiltComponentTemplates = async ( }; const isTemplate = (path: string) => { - const pathParts = Registry.pathParts(path); + const pathParts = getPathParts(path); return pathParts.type === ElasticsearchAssetType.indexTemplate; }; const isComponentTemplate = (path: string) => { - const pathParts = Registry.pathParts(path); + const pathParts = getPathParts(path); return pathParts.type === ElasticsearchAssetType.componentTemplate; }; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/common.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/common.ts index 46f36dba967470..764e1b51f1bcab 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/common.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/common.ts @@ -4,8 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as Registry from '../../registry'; - -export const getAsset = (path: string): Buffer => { - return Registry.getAsset(path); -}; +export { getAsset } from '../../archive'; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts index 1002eedc48740f..9da5e8cd0a937a 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts @@ -7,7 +7,7 @@ import { SavedObjectsClientContract } from 'kibana/server'; import { saveInstalledEsRefs } from '../../packages/install'; -import * as Registry from '../../registry'; +import { getPathParts } from '../../archive'; import { ElasticsearchAssetType, EsAssetReference, @@ -104,7 +104,7 @@ export const installTransform = async ( }; const isTransform = (path: string) => { - const pathParts = Registry.pathParts(path); + const pathParts = getPathParts(path); return !path.endsWith('/') && pathParts.type === ElasticsearchAssetType.transform; }; diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts index e7b251ef133c5b..fe93ed84b32f2b 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts @@ -10,7 +10,7 @@ import { SavedObjectsClientContract, } from 'src/core/server'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common'; -import * as Registry from '../../registry'; +import { getAsset, getPathParts } from '../../archive'; import { AssetType, KibanaAssetType, @@ -57,7 +57,7 @@ const AssetInstallers: Record< }; export async function getKibanaAsset(key: string): Promise { - const buffer = Registry.getAsset(key); + const buffer = getAsset(key); // cache values are buffers. convert to string / JSON return JSON.parse(buffer.toString('utf8')); @@ -117,14 +117,14 @@ export async function getKibanaAssets( ): Promise> { const kibanaAssetTypes = Object.values(KibanaAssetType); const isKibanaAssetType = (path: string) => { - const parts = Registry.pathParts(path); + const parts = getPathParts(path); return parts.service === 'kibana' && (kibanaAssetTypes as string[]).includes(parts.type); }; const filteredPaths = paths .filter(isKibanaAssetType) - .map<[string, AssetParts]>((path) => [path, Registry.pathParts(path)]); + .map<[string, AssetParts]>((path) => [path, getPathParts(path)]); const assetArrays: Array> = []; for (const assetType of kibanaAssetTypes) { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/assets.ts b/x-pack/plugins/fleet/server/services/epm/packages/assets.ts index 2e2090312c9ae4..50d8f2f4d2fb22 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/assets.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/assets.ts @@ -6,7 +6,7 @@ import { InstallablePackage } from '../../../types'; import * as Registry from '../registry'; -import { getArchiveFilelist } from '../archive/cache'; +import { getArchiveFilelist, getAsset } from '../archive'; // paths from RegistryPackage are routes to the assets on EPR // e.g. `/package/nginx/1.2.0/data_stream/access/fields/fields.yml` @@ -59,7 +59,7 @@ export async function getAssetsData( // Gather all asset data const assets = getAssets(packageInfo, filter, datasetName); const entries: Registry.ArchiveEntry[] = assets.map((path) => { - const buffer = Registry.getAsset(path); + const buffer = getAsset(path); return { path, buffer }; }); diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts index a2d5c8147002de..1208ffdaefe4af 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts @@ -5,7 +5,8 @@ */ import { AssetParts } from '../../../types'; -import { getBufferExtractor, pathParts, splitPkgKey } from './index'; +import { getPathParts } from '../archive'; +import { getBufferExtractor, splitPkgKey } from './index'; import { untarBuffer, unzipBuffer } from './extract'; const testPaths = [ @@ -46,7 +47,7 @@ const testPaths = [ test('testPathParts', () => { for (const value of testPaths) { - expect(pathParts(value.path)).toStrictEqual(value.assetParts as AssetParts); + expect(getPathParts(value.path)).toStrictEqual(value.assetParts as AssetParts); } }); diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.ts index 52a1894570b2a2..c35e91bdf580bd 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.ts @@ -8,7 +8,6 @@ import semver from 'semver'; import { Response } from 'node-fetch'; import { URL } from 'url'; import { - AssetParts, AssetsGroupedByServiceByType, CategoryId, CategorySummaryList, @@ -18,8 +17,12 @@ import { RegistrySearchResults, RegistrySearchResult, } from '../../../types'; -import { unpackArchiveToCache } from '../archive'; -import { cacheGet, getArchiveFilelist, setArchiveFilelist } from '../archive'; +import { + getArchiveFilelist, + getPathParts, + setArchiveFilelist, + unpackArchiveToCache, +} from '../archive'; import { fetchUrl, getResponse, getResponseStream } from './requests'; import { streamToBuffer } from './streams'; import { getRegistryUrl } from './registry_url'; @@ -146,36 +149,6 @@ export async function getRegistryPackage( return { paths, registryPackageInfo }; } -export function pathParts(path: string): AssetParts { - let dataset; - - let [pkgkey, service, type, file] = path.split('/'); - - // if it's a data stream - if (service === 'data_stream') { - // save the dataset name - dataset = type; - // drop the `data_stream/dataset-name` portion & re-parse - [pkgkey, service, type, file] = path.replace(`data_stream/${dataset}/`, '').split('/'); - } - - // This is to cover for the fields.yml files inside the "fields" directory - if (file === undefined) { - file = type; - type = 'fields'; - service = ''; - } - - return { - pkgkey, - service, - type, - file, - dataset, - path, - } as AssetParts; -} - export async function ensureCachedArchiveInfo( name: string, version: string, @@ -204,19 +177,12 @@ async function fetchArchiveBuffer( return { archiveBuffer, archivePath }; } -export function getAsset(key: string) { - const buffer = cacheGet(key); - if (buffer === undefined) throw new Error(`Cannot find asset ${key}`); - - return buffer; -} - export function groupPathsByService(paths: string[]): AssetsGroupedByServiceByType { const kibanaAssetTypes = Object.values(KibanaAssetType); // ASK: best way, if any, to avoid `any`? const assets = paths.reduce((map: any, path) => { - const parts = pathParts(path.replace(/^\/package\//, '')); + const parts = getPathParts(path.replace(/^\/package\//, '')); if (parts.service === 'kibana' && kibanaAssetTypes.includes(parts.type)) { if (!map[parts.service]) map[parts.service] = {}; if (!map[parts.service][parts.type]) map[parts.service][parts.type] = []; From b8576ed2ddf5cff3f36ab2163bde00e7ffa51d4c Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 12 Nov 2020 13:13:35 +0100 Subject: [PATCH 14/52] fix truncation issue (#83000) --- .../public/chrome/ui/header/header_breadcrumbs.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx index d52faa87cfecd2..ee3311a94a2027 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { EuiHeaderBreadcrumbs } from '@elastic/eui'; +import { EuiFlexGroup, EuiHeaderBreadcrumbs } from '@elastic/eui'; import classNames from 'classnames'; import React from 'react'; import useObservable from 'react-use/lib/useObservable'; @@ -51,15 +51,14 @@ export function HeaderBreadcrumbs({ appTitle$, breadcrumbs$, breadcrumbsAppendEx ), })); - if (breadcrumbsAppendExtension) { + if (breadcrumbsAppendExtension && crumbs[crumbs.length - 1]) { const lastCrumb = crumbs[crumbs.length - 1]; lastCrumb.text = ( - <> - {lastCrumb.text} - - + +
{lastCrumb.text}
+ +
); } - return ; } From 4d346cdfc01f473dec471b2b6f18671005be6fb3 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Thu, 12 Nov 2020 09:04:08 -0500 Subject: [PATCH 15/52] Add maps_oss folder to code_owners (#83204) --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 62abf281e659f3..b7fb3ff04db710 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -137,6 +137,7 @@ /x-pack/test/functional/es_archives/maps/ @elastic/kibana-gis /x-pack/test/visual_regression/tests/maps/index.js @elastic/kibana-gis #CC# /src/plugins/maps_legacy/ @elastic/kibana-gis +#CC# /src/plugins/maps_oss/ @elastic/kibana-gis #CC# /x-pack/plugins/file_upload @elastic/kibana-gis #CC# /x-pack/plugins/maps_legacy_licensing @elastic/kibana-gis #CC# /src/plugins/home/server/tutorials @elastic/kibana-gis From 3a849ff1040e7e9e522a3c8297539c1b013d762a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Thu, 12 Nov 2020 15:05:08 +0100 Subject: [PATCH 16/52] [Index Management] Add an index template link to data stream details (#82592) * Add index template link to data stream details * Fixed ILM policy link and added a check for index template name after navigation Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../helpers/test_subjects.ts | 1 + .../home/data_streams_tab.helpers.ts | 20 ++++++++++++++ .../home/data_streams_tab.test.ts | 27 +++++++++++++++++++ .../data_stream_detail_panel.tsx | 16 ++++++----- 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts index b5386dec342050..313ebefb85301c 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts @@ -51,4 +51,5 @@ export type TestSubjects = | 'templateList' | 'templatesTab' | 'templateTable' + | 'title' | 'viewButton'; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts index 6bf6c11a37bb41..ab796767487b5a 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts @@ -30,6 +30,7 @@ export interface DataStreamsTabTestBed extends TestBed { clickDeleteActionAt: (index: number) => void; clickConfirmDelete: () => void; clickDeleteDataStreamButton: () => void; + clickDetailPanelIndexTemplateLink: () => void; }; findDeleteActionAt: (index: number) => ReactWrapper; findDeleteConfirmationModal: () => ReactWrapper; @@ -38,6 +39,7 @@ export interface DataStreamsTabTestBed extends TestBed { findEmptyPromptIndexTemplateLink: () => ReactWrapper; findDetailPanelIlmPolicyLink: () => ReactWrapper; findDetailPanelIlmPolicyName: () => ReactWrapper; + findDetailPanelIndexTemplateLink: () => ReactWrapper; } export const setup = async (overridingDependencies: any = {}): Promise => { @@ -143,6 +145,17 @@ export const setup = async (overridingDependencies: any = {}): Promise { + const { component, router, find } = testBed; + const indexTemplateLink = find('indexTemplateLink'); + + await act(async () => { + router.navigateTo(indexTemplateLink.props().href!); + }); + + component.update(); + }; + const findDetailPanel = () => { const { find } = testBed; return find('dataStreamDetailPanel'); @@ -158,6 +171,11 @@ export const setup = async (overridingDependencies: any = {}): Promise { + const { find } = testBed; + return find('indexTemplateLink'); + }; + const findDetailPanelIlmPolicyName = () => { const descriptionList = testBed.component.find(EuiDescriptionListDescription); // ilm policy is the last in the details list @@ -176,6 +194,7 @@ export const setup = async (overridingDependencies: any = {}): Promise { setLoadIndicesResponse, setLoadDataStreamsResponse, setLoadDataStreamResponse, + setLoadTemplateResponse, + setLoadTemplatesResponse, } = httpRequestsMockHelpers; setLoadIndicesResponse([ @@ -103,6 +106,10 @@ describe('Data Streams tab', () => { ]); setLoadDataStreamResponse(dataStreamForDetailPanel); + const indexTemplate = fixtures.getTemplate({ name: 'indexTemplate' }); + setLoadTemplatesResponse({ templates: [indexTemplate], legacyTemplates: [] }); + setLoadTemplateResponse(indexTemplate); + testBed = await setup({ history: createMemoryHistory() }); await act(async () => { testBed.actions.goToDataStreamsList(); @@ -244,6 +251,26 @@ describe('Data Streams tab', () => { dataStreams: ['dataStream1'], }); }); + + test('clicking index template name navigates to the index template details', async () => { + const { + actions: { clickNameAt, clickDetailPanelIndexTemplateLink }, + findDetailPanelIndexTemplateLink, + component, + find, + } = testBed; + + await clickNameAt(0); + + const indexTemplateLink = findDetailPanelIndexTemplateLink(); + expect(indexTemplateLink.text()).toBe('indexTemplate'); + + await clickDetailPanelIndexTemplateLink(); + + component.update(); + expect(find('summaryTab').exists()).toBeTruthy(); + expect(find('title').text().trim()).toBe('indexTemplate'); + }); }); }); diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx index 9ec6993717435a..05d7e97745b9e5 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx @@ -29,9 +29,9 @@ import { useLoadDataStream } from '../../../../services/api'; import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal'; import { humanizeTimeStamp } from '../humanize_time_stamp'; import { useUrlGenerator } from '../../../../services/use_url_generator'; +import { getIndexListUri, getTemplateDetailsLink } from '../../../../services/routing'; import { ILM_PAGES_POLICY_EDIT, ILM_URL_GENERATOR_ID } from '../../../../constants'; import { useAppContext } from '../../../../app_context'; -import { getIndexListUri } from '../../../../..'; interface DetailsListProps { details: Array<{ @@ -207,7 +207,14 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ defaultMessage: 'The index template that configured the data stream and configures its backing indices', }), - content: indexTemplateName, + content: ( + + {indexTemplateName} + + ), }, { name: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.ilmPolicyTitle', { @@ -218,10 +225,7 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ }), content: ilmPolicyName && ilmPolicyLink ? ( - + {ilmPolicyName} ) : ( From 0e7bcf6164c92b3a0a8bedebc3a81885624217bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?= Date: Thu, 12 Nov 2020 15:11:43 +0100 Subject: [PATCH 17/52] [Logs UI] Add pagination to the log stream shared component (#81193) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/http_api/log_entries/entries.ts | 16 +- .../public/components/log_stream/index.tsx | 47 ++++- .../containers/logs/log_entries/index.ts | 18 +- .../containers/logs/log_stream/index.ts | 171 ++++++++++++++++-- .../log_entries/kibana_log_entries_adapter.ts | 25 ++- .../log_entries_domain/log_entries_domain.ts | 52 +++--- .../server/routes/log_entries/entries.ts | 41 +++-- .../server/routes/log_entries/highlights.ts | 2 +- 8 files changed, 299 insertions(+), 73 deletions(-) diff --git a/x-pack/plugins/infra/common/http_api/log_entries/entries.ts b/x-pack/plugins/infra/common/http_api/log_entries/entries.ts index d38b4690fed713..48790c3faca529 100644 --- a/x-pack/plugins/infra/common/http_api/log_entries/entries.ts +++ b/x-pack/plugins/infra/common/http_api/log_entries/entries.ts @@ -99,11 +99,17 @@ export type LogEntryContext = rt.TypeOf; export type LogEntry = rt.TypeOf; export const logEntriesResponseRT = rt.type({ - data: rt.type({ - entries: rt.array(logEntryRT), - topCursor: rt.union([logEntriesCursorRT, rt.null]), - bottomCursor: rt.union([logEntriesCursorRT, rt.null]), - }), + data: rt.intersection([ + rt.type({ + entries: rt.array(logEntryRT), + topCursor: rt.union([logEntriesCursorRT, rt.null]), + bottomCursor: rt.union([logEntriesCursorRT, rt.null]), + }), + rt.partial({ + hasMoreBefore: rt.boolean, + hasMoreAfter: rt.boolean, + }), + ]), }); export type LogEntriesResponse = rt.TypeOf; diff --git a/x-pack/plugins/infra/public/components/log_stream/index.tsx b/x-pack/plugins/infra/public/components/log_stream/index.tsx index 6698018e8cc190..62a4d7ffc3d814 100644 --- a/x-pack/plugins/infra/public/components/log_stream/index.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; +import React, { useMemo, useCallback } from 'react'; import { noop } from 'lodash'; import useMount from 'react-use/lib/useMount'; import { euiStyled } from '../../../../observability/public'; @@ -17,6 +17,8 @@ import { useLogStream } from '../../containers/logs/log_stream'; import { ScrollableLogTextStreamView } from '../logging/log_text_stream'; +const PAGE_THRESHOLD = 2; + export interface LogStreamProps { sourceId?: string; startTimestamp: number; @@ -58,7 +60,16 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re }); // Internal state - const { loadingState, entries, fetchEntries } = useLogStream({ + const { + loadingState, + pageLoadingState, + entries, + hasMoreBefore, + hasMoreAfter, + fetchEntries, + fetchPreviousEntries, + fetchNextEntries, + } = useLogStream({ sourceId, startTimestamp, endTimestamp, @@ -70,6 +81,8 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re const isReloading = isLoadingSourceConfiguration || loadingState === 'uninitialized' || loadingState === 'loading'; + const isLoadingMore = pageLoadingState === 'loading'; + const columnConfigurations = useMemo(() => { return sourceConfiguration ? sourceConfiguration.configuration.logColumns : []; }, [sourceConfiguration]); @@ -84,13 +97,33 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re [entries] ); + const parsedHeight = typeof height === 'number' ? `${height}px` : height; + // Component lifetime useMount(() => { loadSourceConfiguration(); fetchEntries(); }); - const parsedHeight = typeof height === 'number' ? `${height}px` : height; + // Pagination handler + const handlePagination = useCallback( + ({ fromScroll, pagesBeforeStart, pagesAfterEnd }) => { + if (!fromScroll) { + return; + } + + if (isLoadingMore) { + return; + } + + if (pagesBeforeStart < PAGE_THRESHOLD) { + fetchPreviousEntries(); + } else if (pagesAfterEnd < PAGE_THRESHOLD) { + fetchNextEntries(); + } + }, + [isLoadingMore, fetchPreviousEntries, fetchNextEntries] + ); return ( @@ -101,13 +134,13 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re scale="medium" wrap={false} isReloading={isReloading} - isLoadingMore={false} - hasMoreBeforeStart={false} - hasMoreAfterEnd={false} + isLoadingMore={isLoadingMore} + hasMoreBeforeStart={hasMoreBefore} + hasMoreAfterEnd={hasMoreAfter} isStreaming={false} lastLoadedTime={null} jumpToTarget={noop} - reportVisibleInterval={noop} + reportVisibleInterval={handlePagination} loadNewerItems={noop} reloadItems={fetchEntries} highlightedItem={highlight ?? null} diff --git a/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts b/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts index 146746af980c93..bf4c5fbe0b13bf 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts @@ -367,16 +367,16 @@ const logEntriesStateReducer = (prevState: LogEntriesStateParams, action: Action case Action.ReceiveNewEntries: return { ...prevState, - ...action.payload, + entries: action.payload.entries, + topCursor: action.payload.topCursor, + bottomCursor: action.payload.bottomCursor, centerCursor: getCenterCursor(action.payload.entries), lastLoadedTime: new Date(), isReloading: false, - - // Be optimistic. If any of the before/after requests comes empty, set - // the corresponding flag to `false` - hasMoreBeforeStart: true, - hasMoreAfterEnd: true, + hasMoreBeforeStart: action.payload.hasMoreBefore ?? prevState.hasMoreBeforeStart, + hasMoreAfterEnd: action.payload.hasMoreAfter ?? prevState.hasMoreAfterEnd, }; + case Action.ReceiveEntriesBefore: { const newEntries = action.payload.entries; const prevEntries = cleanDuplicateItems(prevState.entries, newEntries); @@ -385,7 +385,7 @@ const logEntriesStateReducer = (prevState: LogEntriesStateParams, action: Action const update = { entries, isLoadingMore: false, - hasMoreBeforeStart: newEntries.length > 0, + hasMoreBeforeStart: action.payload.hasMoreBefore ?? prevState.hasMoreBeforeStart, // Keep the previous cursor if request comes empty, to easily extend the range. topCursor: newEntries.length > 0 ? action.payload.topCursor : prevState.topCursor, centerCursor: getCenterCursor(entries), @@ -402,7 +402,7 @@ const logEntriesStateReducer = (prevState: LogEntriesStateParams, action: Action const update = { entries, isLoadingMore: false, - hasMoreAfterEnd: newEntries.length > 0, + hasMoreAfterEnd: action.payload.hasMoreAfter ?? prevState.hasMoreAfterEnd, // Keep the previous cursor if request comes empty, to easily extend the range. bottomCursor: newEntries.length > 0 ? action.payload.bottomCursor : prevState.bottomCursor, centerCursor: getCenterCursor(entries), @@ -419,6 +419,8 @@ const logEntriesStateReducer = (prevState: LogEntriesStateParams, action: Action topCursor: null, bottomCursor: null, centerCursor: null, + // Assume there are more pages on both ends unless proven wrong by the + // API with an explicit `false` response. hasMoreBeforeStart: true, hasMoreAfterEnd: true, }; diff --git a/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts b/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts index 4a6da6063e9603..566edcce913183 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useState, useMemo } from 'react'; +import { useMemo, useEffect } from 'react'; +import useSetState from 'react-use/lib/useSetState'; +import usePrevious from 'react-use/lib/usePrevious'; import { esKuery } from '../../../../../../../src/plugins/data/public'; import { fetchLogEntries } from '../log_entries/api/fetch_log_entries'; import { useTrackedPromise } from '../../../utils/use_tracked_promise'; @@ -21,19 +23,62 @@ interface LogStreamProps { interface LogStreamState { entries: LogEntry[]; + topCursor: LogEntriesCursor | null; + bottomCursor: LogEntriesCursor | null; + hasMoreBefore: boolean; + hasMoreAfter: boolean; +} + +type LoadingState = 'uninitialized' | 'loading' | 'success' | 'error'; + +interface LogStreamReturn extends LogStreamState { fetchEntries: () => void; - loadingState: 'uninitialized' | 'loading' | 'success' | 'error'; + fetchPreviousEntries: () => void; + fetchNextEntries: () => void; + loadingState: LoadingState; + pageLoadingState: LoadingState; } +const INITIAL_STATE: LogStreamState = { + entries: [], + topCursor: null, + bottomCursor: null, + // Assume there are pages available until the API proves us wrong + hasMoreBefore: true, + hasMoreAfter: true, +}; + +const EMPTY_DATA = { + entries: [], + topCursor: null, + bottomCursor: null, +}; + export function useLogStream({ sourceId, startTimestamp, endTimestamp, query, center, -}: LogStreamProps): LogStreamState { +}: LogStreamProps): LogStreamReturn { const { services } = useKibanaContextForPlugin(); - const [entries, setEntries] = useState([]); + const [state, setState] = useSetState(INITIAL_STATE); + + // Ensure the pagination keeps working when the timerange gets extended + const prevStartTimestamp = usePrevious(startTimestamp); + const prevEndTimestamp = usePrevious(endTimestamp); + + useEffect(() => { + if (prevStartTimestamp && prevStartTimestamp > startTimestamp) { + setState({ hasMoreBefore: true }); + } + }, [prevStartTimestamp, startTimestamp, setState]); + + useEffect(() => { + if (prevEndTimestamp && prevEndTimestamp < endTimestamp) { + setState({ hasMoreAfter: true }); + } + }, [prevEndTimestamp, endTimestamp, setState]); const parsedQuery = useMemo(() => { return query @@ -46,7 +91,7 @@ export function useLogStream({ { cancelPreviousOn: 'creation', createPromise: () => { - setEntries([]); + setState(INITIAL_STATE); const fetchPosition = center ? { center } : { before: 'last' }; return fetchLogEntries( @@ -61,26 +106,130 @@ export function useLogStream({ ); }, onResolve: ({ data }) => { - setEntries(data.entries); + setState((prevState) => ({ + ...data, + hasMoreBefore: data.hasMoreBefore ?? prevState.hasMoreBefore, + hasMoreAfter: data.hasMoreAfter ?? prevState.hasMoreAfter, + })); }, }, [sourceId, startTimestamp, endTimestamp, query] ); - const loadingState = useMemo(() => convertPromiseStateToLoadingState(entriesPromise.state), [ - entriesPromise.state, - ]); + const [previousEntriesPromise, fetchPreviousEntries] = useTrackedPromise( + { + cancelPreviousOn: 'creation', + createPromise: () => { + if (state.topCursor === null) { + throw new Error( + 'useLogState: Cannot fetch previous entries. No cursor is set.\nEnsure you have called `fetchEntries` at least once.' + ); + } + + if (!state.hasMoreBefore) { + return Promise.resolve({ data: EMPTY_DATA }); + } + + return fetchLogEntries( + { + sourceId, + startTimestamp, + endTimestamp, + query: parsedQuery, + before: state.topCursor, + }, + services.http.fetch + ); + }, + onResolve: ({ data }) => { + if (!data.entries.length) { + return; + } + setState((prevState) => ({ + entries: [...data.entries, ...prevState.entries], + hasMoreBefore: data.hasMoreBefore ?? prevState.hasMoreBefore, + topCursor: data.topCursor ?? prevState.topCursor, + })); + }, + }, + [sourceId, startTimestamp, endTimestamp, query, state.topCursor] + ); + + const [nextEntriesPromise, fetchNextEntries] = useTrackedPromise( + { + cancelPreviousOn: 'creation', + createPromise: () => { + if (state.bottomCursor === null) { + throw new Error( + 'useLogState: Cannot fetch next entries. No cursor is set.\nEnsure you have called `fetchEntries` at least once.' + ); + } + + if (!state.hasMoreAfter) { + return Promise.resolve({ data: EMPTY_DATA }); + } + + return fetchLogEntries( + { + sourceId, + startTimestamp, + endTimestamp, + query: parsedQuery, + after: state.bottomCursor, + }, + services.http.fetch + ); + }, + onResolve: ({ data }) => { + if (!data.entries.length) { + return; + } + setState((prevState) => ({ + entries: [...prevState.entries, ...data.entries], + hasMoreAfter: data.hasMoreAfter ?? prevState.hasMoreAfter, + bottomCursor: data.bottomCursor ?? prevState.bottomCursor, + })); + }, + }, + [sourceId, startTimestamp, endTimestamp, query, state.bottomCursor] + ); + + const loadingState = useMemo( + () => convertPromiseStateToLoadingState(entriesPromise.state), + [entriesPromise.state] + ); + + const pageLoadingState = useMemo(() => { + const states = [previousEntriesPromise.state, nextEntriesPromise.state]; + + if (states.includes('pending')) { + return 'loading'; + } + + if (states.includes('rejected')) { + return 'error'; + } + + if (states.includes('resolved')) { + return 'success'; + } + + return 'uninitialized'; + }, [previousEntriesPromise.state, nextEntriesPromise.state]); return { - entries, + ...state, fetchEntries, + fetchPreviousEntries, + fetchNextEntries, loadingState, + pageLoadingState, }; } function convertPromiseStateToLoadingState( state: 'uninitialized' | 'pending' | 'resolved' | 'rejected' -): LogStreamState['loadingState'] { +): LoadingState { switch (state) { case 'uninitialized': return 'uninitialized'; diff --git a/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts index 9309ad85a35708..6ffa1ad4b0b827 100644 --- a/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts @@ -35,8 +35,9 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { sourceConfiguration: InfraSourceConfiguration, fields: string[], params: LogEntriesParams - ): Promise { - const { startTimestamp, endTimestamp, query, cursor, size, highlightTerm } = params; + ): Promise<{ documents: LogEntryDocument[]; hasMoreBefore?: boolean; hasMoreAfter?: boolean }> { + const { startTimestamp, endTimestamp, query, cursor, highlightTerm } = params; + const size = params.size ?? LOG_ENTRIES_PAGE_SIZE; const { sortDirection, searchAfterClause } = processCursor(cursor); @@ -72,7 +73,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { index: sourceConfiguration.logAlias, ignoreUnavailable: true, body: { - size: typeof size !== 'undefined' ? size : LOG_ENTRIES_PAGE_SIZE, + size: size + 1, // Extra one to test if it has more before or after track_total_hits: false, _source: false, fields, @@ -104,8 +105,22 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { esQuery ); - const hits = sortDirection === 'asc' ? esResult.hits.hits : esResult.hits.hits.reverse(); - return mapHitsToLogEntryDocuments(hits, fields); + const hits = esResult.hits.hits; + const hasMore = hits.length > size; + + if (hasMore) { + hits.pop(); + } + + if (sortDirection === 'desc') { + hits.reverse(); + } + + return { + documents: mapHitsToLogEntryDocuments(hits, fields), + hasMoreBefore: sortDirection === 'desc' ? hasMore : undefined, + hasMoreAfter: sortDirection === 'asc' ? hasMore : undefined, + }; } public async getContainedLogSummaryBuckets( diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts index cc9d4c749c77d6..1cf0afd50b80c5 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts @@ -74,7 +74,7 @@ export class InfraLogEntriesDomain { requestContext: RequestHandlerContext, sourceId: string, params: LogEntriesAroundParams - ) { + ): Promise<{ entries: LogEntry[]; hasMoreBefore?: boolean; hasMoreAfter?: boolean }> { const { startTimestamp, endTimestamp, center, query, size, highlightTerm } = params; /* @@ -87,14 +87,18 @@ export class InfraLogEntriesDomain { */ const halfSize = (size || LOG_ENTRIES_PAGE_SIZE) / 2; - const entriesBefore = await this.getLogEntries(requestContext, sourceId, { - startTimestamp, - endTimestamp, - query, - cursor: { before: center }, - size: Math.floor(halfSize), - highlightTerm, - }); + const { entries: entriesBefore, hasMoreBefore } = await this.getLogEntries( + requestContext, + sourceId, + { + startTimestamp, + endTimestamp, + query, + cursor: { before: center }, + size: Math.floor(halfSize), + highlightTerm, + } + ); /* * Elasticsearch's `search_after` returns documents after the specified cursor. @@ -108,23 +112,27 @@ export class InfraLogEntriesDomain { ? entriesBefore[entriesBefore.length - 1].cursor : { time: center.time - 1, tiebreaker: 0 }; - const entriesAfter = await this.getLogEntries(requestContext, sourceId, { - startTimestamp, - endTimestamp, - query, - cursor: { after: cursorAfter }, - size: Math.ceil(halfSize), - highlightTerm, - }); + const { entries: entriesAfter, hasMoreAfter } = await this.getLogEntries( + requestContext, + sourceId, + { + startTimestamp, + endTimestamp, + query, + cursor: { after: cursorAfter }, + size: Math.ceil(halfSize), + highlightTerm, + } + ); - return [...entriesBefore, ...entriesAfter]; + return { entries: [...entriesBefore, ...entriesAfter], hasMoreBefore, hasMoreAfter }; } public async getLogEntries( requestContext: RequestHandlerContext, sourceId: string, params: LogEntriesParams - ): Promise { + ): Promise<{ entries: LogEntry[]; hasMoreBefore?: boolean; hasMoreAfter?: boolean }> { const { configuration } = await this.libs.sources.getSourceConfiguration( requestContext.core.savedObjects.client, sourceId @@ -136,7 +144,7 @@ export class InfraLogEntriesDomain { const requiredFields = getRequiredFields(configuration, messageFormattingRules); - const documents = await this.adapter.getLogEntries( + const { documents, hasMoreBefore, hasMoreAfter } = await this.adapter.getLogEntries( requestContext, configuration, requiredFields, @@ -173,7 +181,7 @@ export class InfraLogEntriesDomain { }; }); - return entries; + return { entries, hasMoreBefore, hasMoreAfter }; } public async getLogSummaryBucketsBetween( @@ -323,7 +331,7 @@ export interface LogEntriesAdapter { sourceConfiguration: InfraSourceConfiguration, fields: string[], params: LogEntriesParams - ): Promise; + ): Promise<{ documents: LogEntryDocument[]; hasMoreBefore?: boolean; hasMoreAfter?: boolean }>; getContainedLogSummaryBuckets( requestContext: RequestHandlerContext, diff --git a/x-pack/plugins/infra/server/routes/log_entries/entries.ts b/x-pack/plugins/infra/server/routes/log_entries/entries.ts index c1f63d9c295772..2baf3fd7aa9901 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/entries.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/entries.ts @@ -34,14 +34,21 @@ export const initLogEntriesRoute = ({ framework, logEntries }: InfraBackendLibs) } = payload; let entries; + let hasMoreBefore; + let hasMoreAfter; + if ('center' in payload) { - entries = await logEntries.getLogEntriesAround(requestContext, sourceId, { - startTimestamp, - endTimestamp, - query: parseFilterQuery(query), - center: payload.center, - size, - }); + ({ entries, hasMoreBefore, hasMoreAfter } = await logEntries.getLogEntriesAround( + requestContext, + sourceId, + { + startTimestamp, + endTimestamp, + query: parseFilterQuery(query), + center: payload.center, + size, + } + )); } else { let cursor: LogEntriesParams['cursor']; if ('before' in payload) { @@ -50,13 +57,17 @@ export const initLogEntriesRoute = ({ framework, logEntries }: InfraBackendLibs) cursor = { after: payload.after }; } - entries = await logEntries.getLogEntries(requestContext, sourceId, { - startTimestamp, - endTimestamp, - query: parseFilterQuery(query), - cursor, - size, - }); + ({ entries, hasMoreBefore, hasMoreAfter } = await logEntries.getLogEntries( + requestContext, + sourceId, + { + startTimestamp, + endTimestamp, + query: parseFilterQuery(query), + cursor, + size, + } + )); } const hasEntries = entries.length > 0; @@ -67,6 +78,8 @@ export const initLogEntriesRoute = ({ framework, logEntries }: InfraBackendLibs) entries, topCursor: hasEntries ? entries[0].cursor : null, bottomCursor: hasEntries ? entries[entries.length - 1].cursor : null, + hasMoreBefore, + hasMoreAfter, }, }), }); diff --git a/x-pack/plugins/infra/server/routes/log_entries/highlights.ts b/x-pack/plugins/infra/server/routes/log_entries/highlights.ts index cc8483fb5c6582..b315d22c471656 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/highlights.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/highlights.ts @@ -79,7 +79,7 @@ export const initLogEntriesHighlightsRoute = ({ framework, logEntries }: InfraBa return response.ok({ body: logEntriesHighlightsResponseRT.encode({ - data: entriesPerHighlightTerm.map((entries) => { + data: entriesPerHighlightTerm.map(({ entries }) => { if (entries.length > 0) { return { entries, From 169dcef2bf478e0ed5014705c12f4cb102a76918 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 12 Nov 2020 15:16:23 +0100 Subject: [PATCH 18/52] [ML] Persisted URL state for the "Anomaly detection jobs" page (#83149) * [ML] table config in the URL state * [ML] fix job list on the management page * [ML] store query filter in the URL * [ML] fix context for the management page * [ML] update module_list_card.tsx in Logs UI * [ML] fix unit tests * [ML] fix unit tests * [ML] fix unit tests * [ML] move utils functions * [ML] url generator to support both job and group ids --- .../MachineLearningLinks/MLLink.test.tsx | 2 +- .../setup_flyout/module_list_card.tsx | 62 ++++-- x-pack/plugins/infra/public/types.ts | 2 + .../ml/common/util/string_utils.test.ts | 22 +- x-pack/plugins/ml/common/util/string_utils.ts | 8 + .../analytics_list/analytics_list.tsx | 6 +- .../job_filter_bar/{index.js => index.ts} | 0 .../job_filter_bar/job_filter_bar.js | 210 ------------------ .../job_filter_bar/job_filter_bar.tsx | 163 ++++++++++++++ .../components/jobs_list/jobs_list.js | 13 +- .../jobs_list_view/jobs_list_view.js | 22 +- .../jobs/jobs_list/components/utils.d.ts | 2 - .../jobs/jobs_list/components/utils.js | 19 -- .../jobs/jobs_list/components/utils.test.ts | 14 +- .../application/jobs/jobs_list/jobs.tsx | 45 +++- .../jobs_list_page/jobs_list_page.tsx | 99 ++++++--- .../anomaly_detection_urls_generator.ts | 19 +- .../ml_url_generator/ml_url_generator.test.ts | 4 +- .../ml_popover/jobs_table/jobs_table.test.tsx | 4 +- 19 files changed, 391 insertions(+), 325 deletions(-) rename x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/{index.js => index.ts} (100%) delete mode 100644 x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js create mode 100644 x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx index be00364cab92e6..30d4bb34ea3455 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx @@ -19,6 +19,6 @@ test('MLLink produces the correct URL', async () => { ); expect(href).toMatchInlineSnapshot( - `"/app/ml/jobs?mlManagement=(groupIds:!(apm),jobId:!(something))&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-5h,to:now-2h))"` + `"/app/ml/jobs?_a=(queryText:'id:(something)%20groups:(apm)')&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-5h,to:now-2h))"` ); }); diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_flyout/module_list_card.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_flyout/module_list_card.tsx index 67f2c8d58ec0d0..39c21fdc228dfc 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_flyout/module_list_card.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_flyout/module_list_card.tsx @@ -6,10 +6,11 @@ import { EuiCard, EuiIcon, EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { SetupStatus } from '../../../../../common/log_analysis'; import { CreateJobButton, RecreateJobButton } from '../../log_analysis_setup/create_job_button'; -import { useLinkProps } from '../../../../hooks/use_link_props'; +import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; +import { mountReactNode } from '../../../../../../../../src/core/public/utils'; export const LogAnalysisModuleListCard: React.FC<{ jobId: string; @@ -26,6 +27,39 @@ export const LogAnalysisModuleListCard: React.FC<{ moduleStatus, onViewSetup, }) => { + const { + services: { + ml, + application: { navigateToUrl }, + notifications: { toasts }, + }, + } = useKibanaContextForPlugin(); + + const [viewInMlLink, setViewInMlLink] = useState(''); + + const getMlUrl = async () => { + if (!ml.urlGenerator) { + toasts.addWarning({ + title: mountReactNode( + + ), + }); + return; + } + setViewInMlLink(await ml.urlGenerator.createUrl({ page: 'jobs', pageState: { jobId } })); + }; + + useEffect(() => { + getMlUrl(); + }); + + const navigateToMlApp = async () => { + await navigateToUrl(viewInMlLink); + }; + const moduleIcon = moduleStatus.type === 'required' ? ( @@ -33,12 +67,6 @@ export const LogAnalysisModuleListCard: React.FC<{ ); - const viewInMlLinkProps = useLinkProps({ - app: 'ml', - pathname: '/jobs', - search: { mlManagement: `(jobId:${jobId})` }, - }); - const moduleSetupButton = moduleStatus.type === 'required' ? ( @@ -50,13 +78,17 @@ export const LogAnalysisModuleListCard: React.FC<{ ) : ( <> - - - - + {viewInMlLink ? ( + <> + + + + + + ) : null} ); diff --git a/x-pack/plugins/infra/public/types.ts b/x-pack/plugins/infra/public/types.ts index 6ff699066eb15a..116345b35fdcea 100644 --- a/x-pack/plugins/infra/public/types.ts +++ b/x-pack/plugins/infra/public/types.ts @@ -18,6 +18,7 @@ import type { ObservabilityPluginStart, } from '../../observability/public'; import type { SpacesPluginStart } from '../../spaces/public'; +import { MlPluginStart } from '../../ml/public'; // Our own setup and start contract values export type InfraClientSetupExports = void; @@ -38,6 +39,7 @@ export interface InfraClientStartDeps { spaces: SpacesPluginStart; triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; usageCollection: UsageCollectionStart; + ml: MlPluginStart; } export type InfraClientCoreSetup = CoreSetup; diff --git a/x-pack/plugins/ml/common/util/string_utils.test.ts b/x-pack/plugins/ml/common/util/string_utils.test.ts index 8afc7e52c9fa50..3503e2be35e867 100644 --- a/x-pack/plugins/ml/common/util/string_utils.test.ts +++ b/x-pack/plugins/ml/common/util/string_utils.test.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { renderTemplate, getMedianStringLength, stringHash } from './string_utils'; +import { + renderTemplate, + getMedianStringLength, + stringHash, + getGroupQueryText, +} from './string_utils'; const strings: string[] = [ 'foo', @@ -54,4 +59,19 @@ describe('ML - string utils', () => { expect(hash1).not.toBe(hash2); }); }); + + describe('getGroupQueryText', () => { + const groupIdOne = 'test_group_id_1'; + const groupIdTwo = 'test_group_id_2'; + + it('should get query string for selected group ids', () => { + const actual = getGroupQueryText([groupIdOne, groupIdTwo]); + expect(actual).toBe(`groups:(${groupIdOne} or ${groupIdTwo})`); + }); + + it('should get query string for selected group id', () => { + const actual = getGroupQueryText([groupIdOne]); + expect(actual).toBe(`groups:(${groupIdOne})`); + }); + }); }); diff --git a/x-pack/plugins/ml/common/util/string_utils.ts b/x-pack/plugins/ml/common/util/string_utils.ts index b4591fd2943e67..4691bac0a065a4 100644 --- a/x-pack/plugins/ml/common/util/string_utils.ts +++ b/x-pack/plugins/ml/common/util/string_utils.ts @@ -39,3 +39,11 @@ export function stringHash(str: string): number { } return hash < 0 ? hash * -2 : hash; } + +export function getGroupQueryText(groupIds: string[]): string { + return `groups:(${groupIds.join(' or ')})`; +} + +export function getJobQueryText(jobIds: string | string[]): string { + return Array.isArray(jobIds) ? `id:(${jobIds.join(' OR ')})` : jobIds; +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 8ed2436843e0ec..17ef84179ce635 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -30,15 +30,13 @@ import { getTaskStateBadge, getJobTypeBadge, useColumns } from './use_columns'; import { ExpandedRow } from './expanded_row'; import { AnalyticStatsBarStats, StatsBar } from '../../../../../components/stats_bar'; import { CreateAnalyticsButton } from '../create_analytics_button'; -import { - getSelectedIdFromUrl, - getGroupQueryText, -} from '../../../../../jobs/jobs_list/components/utils'; +import { getSelectedIdFromUrl } from '../../../../../jobs/jobs_list/components/utils'; import { SourceSelection } from '../source_selection'; import { filterAnalytics } from '../../../../common/search_bar_filters'; import { AnalyticsEmptyPrompt } from './empty_prompt'; import { useTableSettings } from './use_table_settings'; import { RefreshAnalyticsListButton } from '../refresh_analytics_list_button'; +import { getGroupQueryText } from '../../../../../../../common/util/string_utils'; const filters: EuiSearchBarProps['filters'] = [ { diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/index.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/index.ts similarity index 100% rename from x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/index.js rename to x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/index.ts diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js deleted file mode 100644 index 08373542c12346..00000000000000 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js +++ /dev/null @@ -1,210 +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 PropTypes from 'prop-types'; -import React, { Component, Fragment } from 'react'; - -import { ml } from '../../../../services/ml_api_service'; -import { JobGroup } from '../job_group'; -import { - getGroupQueryText, - getSelectedIdFromUrl, - clearSelectedJobIdFromUrl, - getJobQueryText, -} from '../utils'; - -import { EuiSearchBar, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -function loadGroups() { - return ml.jobs - .groups() - .then((groups) => { - return groups.map((g) => ({ - value: g.id, - view: ( -
- -   - - - -
- ), - })); - }) - .catch((error) => { - console.log(error); - return []; - }); -} - -export class JobFilterBar extends Component { - constructor(props) { - super(props); - - this.state = { error: null }; - this.setFilters = props.setFilters; - } - - urlFilterIdCleared = false; - - componentDidMount() { - // If job id is selected in url, filter table to that id - let defaultQueryText; - const { jobId, groupIds } = getSelectedIdFromUrl(window.location.href); - - if (groupIds !== undefined) { - defaultQueryText = getGroupQueryText(groupIds); - } else if (jobId !== undefined) { - defaultQueryText = getJobQueryText(jobId); - } - - if (defaultQueryText !== undefined) { - this.setState( - { - defaultQueryText, - }, - () => { - // trigger onChange with query for job id to trigger table filter - const query = EuiSearchBar.Query.parse(defaultQueryText); - this.onChange({ query }); - } - ); - } - } - - onChange = ({ query, error }) => { - if (error) { - this.setState({ error }); - } else { - if (query.text === '' && this.urlFilterIdCleared === false) { - this.urlFilterIdCleared = true; - clearSelectedJobIdFromUrl(window.location.href); - } - let clauses = []; - if (query && query.ast !== undefined && query.ast.clauses !== undefined) { - clauses = query.ast.clauses; - } - this.setFilters(clauses); - this.setState({ error: null }); - } - }; - - render() { - const { error, defaultQueryText } = this.state; - const filters = [ - { - type: 'field_value_toggle_group', - field: 'job_state', - items: [ - { - value: 'opened', - name: i18n.translate('xpack.ml.jobsList.jobFilterBar.openedLabel', { - defaultMessage: 'Opened', - }), - }, - { - value: 'closed', - name: i18n.translate('xpack.ml.jobsList.jobFilterBar.closedLabel', { - defaultMessage: 'Closed', - }), - }, - { - value: 'failed', - name: i18n.translate('xpack.ml.jobsList.jobFilterBar.failedLabel', { - defaultMessage: 'Failed', - }), - }, - ], - }, - { - type: 'field_value_toggle_group', - field: 'datafeed_state', - items: [ - { - value: 'started', - name: i18n.translate('xpack.ml.jobsList.jobFilterBar.startedLabel', { - defaultMessage: 'Started', - }), - }, - { - value: 'stopped', - name: i18n.translate('xpack.ml.jobsList.jobFilterBar.stoppedLabel', { - defaultMessage: 'Stopped', - }), - }, - ], - }, - { - type: 'field_value_selection', - field: 'groups', - name: i18n.translate('xpack.ml.jobsList.jobFilterBar.groupLabel', { - defaultMessage: 'Group', - }), - multiSelect: 'or', - cache: 10000, - options: () => loadGroups(), - }, - ]; - // if prop flag for default filter set to true - // set defaultQuery to job id and force trigger filter with onChange - pass it the query object for the job id - return ( - - - {defaultQueryText === undefined && ( - - )} - {defaultQueryText !== undefined && ( - - )} - - - - - - ); - } -} -JobFilterBar.propTypes = { - setFilters: PropTypes.func.isRequired, -}; - -function getError(error) { - if (error) { - return i18n.translate('xpack.ml.jobsList.jobFilterBar.invalidSearchErrorMessage', { - defaultMessage: 'Invalid search: {errorMessage}', - values: { errorMessage: error.message }, - }); - } - - return ''; -} diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx new file mode 100644 index 00000000000000..f0fa62b7a3d8ae --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, Fragment, useCallback, useEffect, useMemo, useState } from 'react'; + +import { + EuiSearchBar, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + SearchFilterConfig, + EuiSearchBarProps, + Query, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +// @ts-ignore +import { JobGroup } from '../job_group'; +import { useMlKibana } from '../../../../contexts/kibana'; + +interface JobFilterBarProps { + jobId: string; + groupIds: string[]; + setFilters: (query: Query | null) => void; + queryText?: string; +} + +export const JobFilterBar: FC = ({ queryText, setFilters }) => { + const [error, setError] = useState(null); + const { + services: { + mlServices: { mlApiServices }, + }, + } = useMlKibana(); + + const loadGroups = useCallback(async () => { + try { + const response = await mlApiServices.jobs.groups(); + return response.map((g: any) => ({ + value: g.id, + view: ( +
+ +   + + + +
+ ), + })); + } catch (e) { + return []; + } + }, []); + + const queryInstance: Query = useMemo(() => { + return EuiSearchBar.Query.parse(queryText ?? ''); + }, [queryText]); + + const onChange: EuiSearchBarProps['onChange'] = ({ query, error: queryError }) => { + if (error) { + setError(queryError); + } else { + setFilters(query); + setError(null); + } + }; + + useEffect(() => { + setFilters(queryInstance); + }, []); + + const filters: SearchFilterConfig[] = useMemo( + () => [ + { + type: 'field_value_toggle_group', + field: 'job_state', + items: [ + { + value: 'opened', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.openedLabel', { + defaultMessage: 'Opened', + }), + }, + { + value: 'closed', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.closedLabel', { + defaultMessage: 'Closed', + }), + }, + { + value: 'failed', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.failedLabel', { + defaultMessage: 'Failed', + }), + }, + ], + }, + { + type: 'field_value_toggle_group', + field: 'datafeed_state', + items: [ + { + value: 'started', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.startedLabel', { + defaultMessage: 'Started', + }), + }, + { + value: 'stopped', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.stoppedLabel', { + defaultMessage: 'Stopped', + }), + }, + ], + }, + { + type: 'field_value_selection', + field: 'groups', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.groupLabel', { + defaultMessage: 'Group', + }), + multiSelect: 'or', + cache: 10000, + options: () => loadGroups(), + }, + ], + [] + ); + + const errorText = useMemo(() => { + if (error === null) return ''; + + return i18n.translate('xpack.ml.jobsList.jobFilterBar.invalidSearchErrorMessage', { + defaultMessage: 'Invalid search: {errorMessage}', + values: { errorMessage: error.message }, + }); + }, [error]); + + return ( + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js index c0abab6b52cf18..8a05cd51e4d658 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js @@ -22,7 +22,6 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { AnomalyDetectionJobIdLink } from './job_id_link'; -const PAGE_SIZE = 10; const PAGE_SIZE_OPTIONS = [10, 25, 50]; // 'isManagementTable' bool prop to determine when to configure table for use in Kibana management page @@ -32,11 +31,7 @@ export class JobsList extends Component { this.state = { jobsSummaryList: props.jobsSummaryList, - pageIndex: 0, - pageSize: PAGE_SIZE, itemIdToExpandedRowMap: {}, - sortField: 'id', - sortDirection: 'asc', }; } @@ -54,7 +49,7 @@ export class JobsList extends Component { const { field: sortField, direction: sortDirection } = sort; - this.setState({ + this.props.onJobsViewStateUpdate({ pageIndex, pageSize, sortField, @@ -88,7 +83,7 @@ export class JobsList extends Component { pageStart = Math.floor((listLength - 1) / size) * size; // set the state out of the render cycle setTimeout(() => { - this.setState({ + this.props.onJobsViewStateUpdate({ pageIndex: pageStart / size, }); }, 0); @@ -298,7 +293,7 @@ export class JobsList extends Component { }); } - const { pageIndex, pageSize, sortField, sortDirection } = this.state; + const { pageIndex, pageSize, sortField, sortDirection } = this.props.jobsViewState; const { pageOfItems, totalItemCount } = this.getPageOfJobs( pageIndex, @@ -368,6 +363,8 @@ JobsList.propTypes = { refreshJobs: PropTypes.func, selectedJobsCount: PropTypes.number.isRequired, loading: PropTypes.bool, + jobsViewState: PropTypes.object, + onJobsViewStateUpdate: PropTypes.func, }; JobsList.defaultProps = { isManagementTable: false, diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index 9eb7a03f0f5d74..570172abb28c1e 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -222,8 +222,14 @@ export class JobsListView extends Component { this.setState({ selectedJobs }); } - setFilters = (filterClauses) => { + setFilters = (query) => { + const filterClauses = (query && query.ast && query.ast.clauses) || []; const filteredJobsSummaryList = filterJobs(this.state.jobsSummaryList, filterClauses); + + this.props.onJobsViewStateUpdate({ + queryText: query?.text, + }); + this.setState({ filteredJobsSummaryList, filterClauses }, () => { this.refreshSelectedJobs(); }); @@ -358,7 +364,10 @@ export class JobsListView extends Component {
- +
@@ -434,7 +445,10 @@ export class JobsListView extends Component { showDeleteJobModal={this.showDeleteJobModal} refreshJobs={() => this.refreshJobSummaryList(true)} /> - + this.refreshJobSummaryList(true)} + jobsViewState={this.props.jobsViewState} + onJobsViewStateUpdate={this.props.onJobsViewStateUpdate} selectedJobsCount={this.state.selectedJobs.length} loading={loading} /> diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts index 75d6b149fda08b..b781199c852370 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts @@ -5,6 +5,4 @@ */ export function getSelectedIdFromUrl(str: string): { groupIds?: string[]; jobId?: string }; -export function getGroupQueryText(arr: string[]): string; -export function getJobQueryText(arr: string | string[]): string; export function clearSelectedJobIdFromUrl(str: string): void; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js index bc85153928a4b6..397062248689d2 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js @@ -395,22 +395,3 @@ export function getSelectedIdFromUrl(url) { } return result; } - -export function getGroupQueryText(groupIds) { - return `groups:(${groupIds.join(' or ')})`; -} - -export function getJobQueryText(jobIds) { - return Array.isArray(jobIds) ? `id:(${jobIds.join(' OR ')})` : jobIds; -} - -export function clearSelectedJobIdFromUrl(url) { - if (typeof url === 'string') { - url = decodeURIComponent(url); - if (url.includes('mlManagement') && (url.includes('jobId') || url.includes('groupIds'))) { - const urlParams = getUrlVars(url); - const clearedParams = `jobs?_g=${urlParams._g}`; - window.history.replaceState({}, document.title, clearedParams); - } - } -} diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.test.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.test.ts index e4c3c21c5a54a7..4414be0b4fdcbf 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.test.ts +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getGroupQueryText, getSelectedIdFromUrl } from './utils'; +import { getSelectedIdFromUrl } from './utils'; describe('ML - Jobs List utils', () => { const jobId = 'test_job_id_1'; @@ -32,16 +32,4 @@ describe('ML - Jobs List utils', () => { expect(actual).toStrictEqual(expected); }); }); - - describe('getGroupQueryText', () => { - it('should get query string for selected group ids', () => { - const actual = getGroupQueryText([groupIdOne, groupIdTwo]); - expect(actual).toBe(`groups:(${groupIdOne} or ${groupIdTwo})`); - }); - - it('should get query string for selected group id', () => { - const actual = getGroupQueryText([groupIdOne]); - expect(actual).toBe(`groups:(${groupIdOne})`); - }); - }); }); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx index 1e45f285945721..4c6469f6800a79 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; - +import React, { FC, useCallback, useMemo } from 'react'; import { NavigationMenu } from '../../components/navigation_menu'; - // @ts-ignore import { JobsListView } from './components/jobs_list_view/index'; +import { useUrlState } from '../../util/url_state'; interface JobsPageProps { blockRefresh?: boolean; @@ -18,11 +17,49 @@ interface JobsPageProps { lastRefresh?: number; } +export interface AnomalyDetectionJobsListState { + pageSize: number; + pageIndex: number; + sortField: string; + sortDirection: string; + queryText?: string; +} + +export const getDefaultAnomalyDetectionJobsListState = (): AnomalyDetectionJobsListState => ({ + pageIndex: 0, + pageSize: 10, + sortField: 'id', + sortDirection: 'asc', +}); + export const JobsPage: FC = (props) => { + const [appState, setAppState] = useUrlState('_a'); + + const jobListState: AnomalyDetectionJobsListState = useMemo(() => { + return { + ...getDefaultAnomalyDetectionJobsListState(), + ...(appState ?? {}), + }; + }, [appState]); + + const onJobsViewStateUpdate = useCallback( + (update: Partial) => { + setAppState({ + ...jobListState, + ...update, + }); + }, + [appState, setAppState] + ); + return (
- +
); }; diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index 61dfea8897e82f..ad4b9ad78902bd 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useState, Fragment, FC } from 'react'; +import React, { useEffect, useState, Fragment, FC, useMemo, useCallback } from 'react'; import { Router } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { CoreStart } from 'kibana/public'; @@ -35,6 +35,11 @@ import { JobsListView } from '../../../../jobs/jobs_list/components/jobs_list_vi import { DataFrameAnalyticsList } from '../../../../data_frame_analytics/pages/analytics_management/components/analytics_list'; import { AccessDeniedPage } from '../access_denied_page'; import { SharePluginStart } from '../../../../../../../../../src/plugins/share/public'; +import { + AnomalyDetectionJobsListState, + getDefaultAnomalyDetectionJobsListState, +} from '../../../../jobs/jobs_list/jobs'; +import { getMlGlobalServices } from '../../../../app'; interface Tab { 'data-test-subj': string; @@ -43,38 +48,60 @@ interface Tab { content: any; } -function getTabs(isMlEnabledInSpace: boolean): Tab[] { - return [ - { - 'data-test-subj': 'mlStackManagementJobsListAnomalyDetectionTab', - id: 'anomaly_detection_jobs', - name: i18n.translate('xpack.ml.management.jobsList.anomalyDetectionTab', { - defaultMessage: 'Anomaly detection', - }), - content: ( - - - - - ), - }, - { - 'data-test-subj': 'mlStackManagementJobsListAnalyticsTab', - id: 'analytics_jobs', - name: i18n.translate('xpack.ml.management.jobsList.analyticsTab', { - defaultMessage: 'Analytics', - }), - content: ( - - - - - ), +function useTabs(isMlEnabledInSpace: boolean): Tab[] { + const [jobsViewState, setJobsViewState] = useState( + getDefaultAnomalyDetectionJobsListState() + ); + + const updateState = useCallback( + (update: Partial) => { + setJobsViewState({ + ...jobsViewState, + ...update, + }); }, - ]; + [jobsViewState] + ); + + return useMemo( + () => [ + { + 'data-test-subj': 'mlStackManagementJobsListAnomalyDetectionTab', + id: 'anomaly_detection_jobs', + name: i18n.translate('xpack.ml.management.jobsList.anomalyDetectionTab', { + defaultMessage: 'Anomaly detection', + }), + content: ( + + + + + ), + }, + { + 'data-test-subj': 'mlStackManagementJobsListAnalyticsTab', + id: 'analytics_jobs', + name: i18n.translate('xpack.ml.management.jobsList.analyticsTab', { + defaultMessage: 'Analytics', + }), + content: ( + + + + + ), + }, + ], + [isMlEnabledInSpace, jobsViewState, updateState] + ); } export const JobsListPage: FC<{ @@ -85,7 +112,7 @@ export const JobsListPage: FC<{ const [initialized, setInitialized] = useState(false); const [accessDenied, setAccessDenied] = useState(false); const [isMlEnabledInSpace, setIsMlEnabledInSpace] = useState(false); - const tabs = getTabs(isMlEnabledInSpace); + const tabs = useTabs(isMlEnabledInSpace); const [currentTabId, setCurrentTabId] = useState(tabs[0].id); const I18nContext = coreStart.i18n.Context; @@ -129,7 +156,7 @@ export const JobsListPage: FC<{ setCurrentTabId(id); }} size="s" - tabs={getTabs(isMlEnabledInSpace)} + tabs={tabs} initialSelectedTab={tabs[0]} /> ); @@ -142,7 +169,9 @@ export const JobsListPage: FC<{ return ( - + = { + ...(queryTextArr.length > 0 ? { queryText: queryTextArr.join(' ') } : {}), }; - url = setStateToKbnUrl( - 'mlManagement', + url = setStateToKbnUrl>( + '_a', queryState, { useHash: false, storeInHashQuery: false }, url diff --git a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts index 754f5bec57a07b..e7f12ead3ffe96 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts @@ -30,7 +30,7 @@ describe('MlUrlGenerator', () => { jobId: 'fq_single_1', }, }); - expect(url).toBe('/app/ml/jobs?mlManagement=(jobId:fq_single_1)'); + expect(url).toBe('/app/ml/jobs?_a=(queryText:fq_single_1)'); }); it('should generate valid URL for the Anomaly Detection job management page for groupIds', async () => { @@ -40,7 +40,7 @@ describe('MlUrlGenerator', () => { groupIds: ['farequote', 'categorization'], }, }); - expect(url).toBe('/app/ml/jobs?mlManagement=(groupIds:!(farequote,categorization))'); + expect(url).toBe("/app/ml/jobs?_a=(queryText:'groups:(farequote%20or%20categorization)')"); }); it('should generate valid URL for the page for selecting the type of anomaly detection job to create', async () => { diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx index 156475f63aa65f..b0965f87085583 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx @@ -55,7 +55,7 @@ describe('JobsTableComponent', () => { '[data-test-subj="jobs-table-link"]' ); await waitFor(() => - expect(href).toEqual('/app/ml/jobs?mlManagement=(jobId:linux_anomalous_network_activity_ecs)') + expect(href).toEqual('/app/ml/jobs?_a=(queryText:linux_anomalous_network_activity_ecs)') ); }); @@ -72,7 +72,7 @@ describe('JobsTableComponent', () => { '[data-test-subj="jobs-table-link"]' ); await waitFor(() => - expect(href).toEqual("/app/ml/jobs?mlManagement=(jobId:'job%20id%20with%20spaces')") + expect(href).toEqual("/app/ml/jobs?_a=(queryText:'job%20id%20with%20spaces')") ); }); From 35656b9921d1a0adcfeb1ad2ca54f9d96aa601f4 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 12 Nov 2020 08:28:35 -0600 Subject: [PATCH 19/52] Add additional sources routes (#83227) These were missed in #83125 --- .../routes/workplace_search/sources.test.ts | 188 ++++++++++++++++++ .../server/routes/workplace_search/sources.ts | 124 ++++++++++++ 2 files changed, 312 insertions(+) diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts index 9f2b4121351bc9..6d22002222a666 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts @@ -7,6 +7,8 @@ import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; import { + registerAccountSourcesRoute, + registerAccountSourcesStatusRoute, registerAccountSourceRoute, registerAccountCreateSourceRoute, registerAccountSourceDocumentsRoute, @@ -15,6 +17,9 @@ import { registerAccountSourceSettingsRoute, registerAccountPreSourceRoute, registerAccountPrepareSourcesRoute, + registerAccountSourceSearchableRoute, + registerOrgSourcesRoute, + registerOrgSourcesStatusRoute, registerOrgSourceRoute, registerOrgCreateSourceRoute, registerOrgSourceDocumentsRoute, @@ -23,6 +28,7 @@ import { registerOrgSourceSettingsRoute, registerOrgPreSourceRoute, registerOrgPrepareSourcesRoute, + registerOrgSourceSearchableRoute, registerOrgSourceOauthConfigurationsRoute, registerOrgSourceOauthConfigurationRoute, } from './sources'; @@ -38,6 +44,60 @@ const mockConfig = { }; describe('sources routes', () => { + describe('GET /api/workplace_search/account/sources', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/account/sources', + payload: 'params', + }); + + registerAccountSourcesRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + mockRouter.callRoute({}); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/sources', + }); + }); + }); + + describe('GET /api/workplace_search/account/sources/status', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/account/sources/status', + payload: 'params', + }); + + registerAccountSourcesStatusRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + mockRouter.callRoute({}); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/sources/status', + }); + }); + }); + describe('GET /api/workplace_search/account/sources/{id}', () => { let mockRouter: MockRouter; @@ -351,6 +411,97 @@ describe('sources routes', () => { }); }); + describe('PUT /api/workplace_search/sources/{id}/searchable', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'put', + path: '/api/workplace_search/sources/{id}/searchable', + payload: 'body', + }); + + registerAccountSourceSearchableRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + const mockRequest = { + params: { + id: '123', + }, + body: { + searchable: true, + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/sources/123/searchable', + body: mockRequest.body, + }); + }); + }); + + describe('GET /api/workplace_search/org/sources', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/org/sources', + payload: 'params', + }); + + registerOrgSourcesRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + mockRouter.callRoute({}); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/sources', + }); + }); + }); + + describe('GET /api/workplace_search/org/sources/status', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/org/sources/status', + payload: 'params', + }); + + registerOrgSourcesStatusRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + mockRouter.callRoute({}); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/sources/status', + }); + }); + }); + describe('GET /api/workplace_search/org/sources/{id}', () => { let mockRouter: MockRouter; @@ -664,6 +815,43 @@ describe('sources routes', () => { }); }); + describe('PUT /api/workplace_search/org/sources/{id}/searchable', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'put', + path: '/api/workplace_search/org/sources/{id}/searchable', + payload: 'body', + }); + + registerOrgSourceSearchableRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + const mockRequest = { + params: { + id: '123', + }, + body: { + searchable: true, + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/sources/123/searchable', + body: mockRequest.body, + }); + }); + }); + describe('GET /api/workplace_search/org/settings/connectors', () => { let mockRouter: MockRouter; diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index f496628d02379f..efef53440117e7 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -25,6 +25,40 @@ const oAuthConfigSchema = schema.object({ consumer_key: schema.string(), }); +export function registerAccountSourcesRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/account/sources', + validate: false, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: '/ws/sources', + })(context, request, response); + } + ); +} + +export function registerAccountSourcesStatusRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/account/sources/status', + validate: false, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: '/ws/sources/status', + })(context, request, response); + } + ); +} + export function registerAccountSourceRoute({ router, enterpriseSearchRequestHandler, @@ -228,6 +262,65 @@ export function registerAccountPrepareSourcesRoute({ ); } +export function registerAccountSourceSearchableRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.put( + { + path: '/api/workplace_search/sources/{id}/searchable', + validate: { + body: schema.object({ + searchable: schema.boolean(), + }), + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/ws/sources/${request.params.id}/searchable`, + body: request.body, + })(context, request, response); + } + ); +} + +export function registerOrgSourcesRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/org/sources', + validate: false, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/sources', + })(context, request, response); + } + ); +} + +export function registerOrgSourcesStatusRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/org/sources/status', + validate: false, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/sources/status', + })(context, request, response); + } + ); +} + export function registerOrgSourceRoute({ router, enterpriseSearchRequestHandler, @@ -431,6 +524,31 @@ export function registerOrgPrepareSourcesRoute({ ); } +export function registerOrgSourceSearchableRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.put( + { + path: '/api/workplace_search/org/sources/{id}/searchable', + validate: { + body: schema.object({ + searchable: schema.boolean(), + }), + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/ws/org/sources/${request.params.id}/searchable`, + body: request.body, + })(context, request, response); + } + ); +} + export function registerOrgSourceOauthConfigurationsRoute({ router, enterpriseSearchRequestHandler, @@ -522,6 +640,8 @@ export function registerOrgSourceOauthConfigurationRoute({ } export const registerSourcesRoutes = (dependencies: RouteDependencies) => { + registerAccountSourcesRoute(dependencies); + registerAccountSourcesStatusRoute(dependencies); registerAccountSourceRoute(dependencies); registerAccountCreateSourceRoute(dependencies); registerAccountSourceDocumentsRoute(dependencies); @@ -530,6 +650,9 @@ export const registerSourcesRoutes = (dependencies: RouteDependencies) => { registerAccountSourceSettingsRoute(dependencies); registerAccountPreSourceRoute(dependencies); registerAccountPrepareSourcesRoute(dependencies); + registerAccountSourceSearchableRoute(dependencies); + registerOrgSourcesRoute(dependencies); + registerOrgSourcesStatusRoute(dependencies); registerOrgSourceRoute(dependencies); registerOrgCreateSourceRoute(dependencies); registerOrgSourceDocumentsRoute(dependencies); @@ -538,6 +661,7 @@ export const registerSourcesRoutes = (dependencies: RouteDependencies) => { registerOrgSourceSettingsRoute(dependencies); registerOrgPreSourceRoute(dependencies); registerOrgPrepareSourcesRoute(dependencies); + registerOrgSourceSearchableRoute(dependencies); registerOrgSourceOauthConfigurationsRoute(dependencies); registerOrgSourceOauthConfigurationRoute(dependencies); }; From c3e57943ad377e29b9d6f4a2508a7d7ed1e0f06f Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Thu, 12 Nov 2020 09:32:22 -0500 Subject: [PATCH 20/52] [alerts] add executionStatus to event log doc for action execute (#82401) resolves https://github.com/elastic/kibana/issues/79785 Until now, the execution status was available in the the event log document for the execute action. In this PR we add it. The event log is extended to add the following fields: - `kibana.alerting.status` - from executionStatus.status - `event.reason` - from executionStatus.error.reason The date from the executionStatus and start date in the event log will be set to the same value. Previously, errors encountered while trying to execute an alert executor, eg decrypting the alert, would not end up with an event doc generated. Now they will. In addition, there were a few places where events that could have had the action group in them did not, and one where the instance id was undefined - those were fixed up. --- .../server/alert_instance/alert_instance.ts | 4 + .../create_execution_handler.test.ts | 1 + .../task_runner/create_execution_handler.ts | 1 + .../server/task_runner/task_runner.test.ts | 231 ++++++++++++++---- .../alerts/server/task_runner/task_runner.ts | 115 ++++++--- .../plugins/event_log/generated/mappings.json | 8 + x-pack/plugins/event_log/generated/schemas.ts | 4 +- x-pack/plugins/event_log/scripts/mappings.js | 6 + .../event_log/server/event_logger.test.ts | 6 +- .../plugins/event_log/server/event_logger.ts | 7 +- .../tests/alerting/event_log.ts | 84 +++++++ .../tests/alerting/index.ts | 1 + .../spaces_only/tests/alerting/event_log.ts | 106 +++++--- 13 files changed, 442 insertions(+), 132 deletions(-) create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts diff --git a/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts b/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts index 661fb75f81c007..01790b2a4a0c08 100644 --- a/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts +++ b/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts @@ -51,6 +51,10 @@ export class AlertInstance< return false; } + getLastScheduledActions() { + return this.meta.lastScheduledActions; + } + getScheduledActionOptions() { return this.scheduledExecutionOptions; } diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts index 2f0754d34492fb..ed73fec24db261 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts @@ -113,6 +113,7 @@ test('enqueues execution per selected action', async () => { }, "kibana": Object { "alerting": Object { + "action_group_id": "default", "instance_id": "2", }, "saved_objects": Array [ diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts index 21e642d228b4dc..f49310c42c247a 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts @@ -116,6 +116,7 @@ export function createExecutionHandler({ kibana: { alerting: { instance_id: alertInstanceId, + action_group_id: actionGroup, }, saved_objects: [ { rel: SAVED_OBJECT_REL_PRIMARY, type: 'alert', id: alertId, ...namespace }, diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index 4d0d69010914e2..859b6ec4362cef 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -184,11 +184,15 @@ describe('Task Runner', () => { expect(eventLogger.logEvent).toHaveBeenCalledTimes(1); expect(eventLogger.logEvent.mock.calls[0][0]).toMatchInlineSnapshot(` Object { + "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", "outcome": "success", }, "kibana": Object { + "alerting": Object { + "status": "ok", + }, "saved_objects": Array [ Object { "id": "1", @@ -249,29 +253,13 @@ describe('Task Runner', () => { const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(4); - expect(eventLogger.logEvent).toHaveBeenCalledWith({ - event: { - action: 'execute', - outcome: 'success', - }, - kibana: { - saved_objects: [ - { - id: '1', - namespace: undefined, - rel: 'primary', - type: 'alert', - }, - ], - }, - message: "alert executed: test:1: 'alert-name'", - }); - expect(eventLogger.logEvent).toHaveBeenCalledWith({ + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { event: { action: 'new-instance', }, kibana: { alerting: { + action_group_id: 'default', instance_id: '1', }, saved_objects: [ @@ -285,7 +273,7 @@ describe('Task Runner', () => { }, message: "test:1: 'alert-name' created new instance: '1'", }); - expect(eventLogger.logEvent).toHaveBeenCalledWith({ + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, { event: { action: 'active-instance', }, @@ -305,13 +293,14 @@ describe('Task Runner', () => { }, message: "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", }); - expect(eventLogger.logEvent).toHaveBeenCalledWith({ + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(3, { event: { action: 'execute-action', }, kibana: { alerting: { instance_id: '1', + action_group_id: 'default', }, saved_objects: [ { @@ -330,6 +319,27 @@ describe('Task Runner', () => { message: "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: action:1", }); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(4, { + '@timestamp': '1970-01-01T00:00:00.000Z', + event: { + action: 'execute', + outcome: 'success', + }, + kibana: { + alerting: { + status: 'active', + }, + saved_objects: [ + { + id: '1', + namespace: undefined, + rel: 'primary', + type: 'alert', + }, + ], + }, + message: "alert executed: test:1: 'alert-name'", + }); }); test('includes the apiKey in the request used to initialize the actionsClient', async () => { @@ -402,10 +412,13 @@ describe('Task Runner', () => { Array [ Object { "event": Object { - "action": "execute", - "outcome": "success", + "action": "new-instance", }, "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "1", + }, "saved_objects": Array [ Object { "id": "1", @@ -415,17 +428,17 @@ describe('Task Runner', () => { }, ], }, - "message": "alert executed: test:1: 'alert-name'", + "message": "test:1: 'alert-name' created new instance: '1'", }, ], Array [ Object { "event": Object { - "action": "new-instance", + "action": "active-instance", }, "kibana": Object { "alerting": Object { - "action_group_id": undefined, + "action_group_id": "default", "instance_id": "1", }, "saved_objects": Array [ @@ -437,13 +450,13 @@ describe('Task Runner', () => { }, ], }, - "message": "test:1: 'alert-name' created new instance: '1'", + "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", }, ], Array [ Object { "event": Object { - "action": "active-instance", + "action": "execute-action", }, "kibana": Object { "alerting": Object { @@ -457,19 +470,26 @@ describe('Task Runner', () => { "rel": "primary", "type": "alert", }, + Object { + "id": "1", + "namespace": undefined, + "type": "action", + }, ], }, - "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + "message": "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: action:1", }, ], Array [ Object { + "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { - "action": "execute-action", + "action": "execute", + "outcome": "success", }, "kibana": Object { "alerting": Object { - "instance_id": "1", + "status": "active", }, "saved_objects": Array [ Object { @@ -478,14 +498,9 @@ describe('Task Runner', () => { "rel": "primary", "type": "alert", }, - Object { - "id": "1", - "namespace": undefined, - "type": "action", - }, ], }, - "message": "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: action:1", + "message": "alert executed: test:1: 'alert-name'", }, ], ] @@ -498,6 +513,7 @@ describe('Task Runner', () => { executorServices.alertInstanceFactory('1').scheduleActions('default'); } ); + const date = new Date().toISOString(); const taskRunner = new TaskRunner( alertType, { @@ -505,8 +521,14 @@ describe('Task Runner', () => { state: { ...mockedTaskInstance.state, alertInstances: { - '1': { meta: {}, state: { bar: false } }, - '2': { meta: {}, state: { bar: false } }, + '1': { + meta: { lastScheduledActions: { group: 'default', date } }, + state: { bar: false }, + }, + '2': { + meta: { lastScheduledActions: { group: 'default', date } }, + state: { bar: false }, + }, }, }, }, @@ -545,10 +567,13 @@ describe('Task Runner', () => { Array [ Object { "event": Object { - "action": "execute", - "outcome": "success", + "action": "resolved-instance", }, "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "2", + }, "saved_objects": Array [ Object { "id": "1", @@ -558,18 +583,18 @@ describe('Task Runner', () => { }, ], }, - "message": "alert executed: test:1: 'alert-name'", + "message": "test:1: 'alert-name' resolved instance: '2'", }, ], Array [ Object { "event": Object { - "action": "resolved-instance", + "action": "active-instance", }, "kibana": Object { "alerting": Object { - "action_group_id": undefined, - "instance_id": "2", + "action_group_id": "default", + "instance_id": "1", }, "saved_objects": Array [ Object { @@ -580,18 +605,19 @@ describe('Task Runner', () => { }, ], }, - "message": "test:1: 'alert-name' resolved instance: '2'", + "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", }, ], Array [ Object { + "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { - "action": "active-instance", + "action": "execute", + "outcome": "success", }, "kibana": Object { "alerting": Object { - "action_group_id": "default", - "instance_id": "1", + "status": "active", }, "saved_objects": Array [ Object { @@ -602,7 +628,7 @@ describe('Task Runner', () => { }, ], }, - "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + "message": "alert executed: test:1: 'alert-name'", }, ], ] @@ -787,14 +813,19 @@ describe('Task Runner', () => { Array [ Array [ Object { + "@timestamp": "1970-01-01T00:00:00.000Z", "error": Object { "message": "OMG", }, "event": Object { "action": "execute", "outcome": "failure", + "reason": "execute", }, "kibana": Object { + "alerting": Object { + "status": "error", + }, "saved_objects": Array [ Object { "id": "1", @@ -834,6 +865,40 @@ describe('Task Runner', () => { "state": Object {}, } `); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(1); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "error": Object { + "message": "OMG", + }, + "event": Object { + "action": "execute", + "outcome": "failure", + "reason": "decrypt", + }, + "kibana": Object { + "alerting": Object { + "status": "error", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + }, + ], + }, + "message": "test:1: execution failed", + }, + ], + ] + `); }); test('recovers gracefully when the Alert Task Runner throws an exception when getting internal Services', async () => { @@ -867,6 +932,40 @@ describe('Task Runner', () => { "state": Object {}, } `); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(1); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "error": Object { + "message": "OMG", + }, + "event": Object { + "action": "execute", + "outcome": "failure", + "reason": "unknown", + }, + "kibana": Object { + "alerting": Object { + "status": "error", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + }, + ], + }, + "message": "test:1: execution failed", + }, + ], + ] + `); }); test('recovers gracefully when the Alert Task Runner throws an exception when fetching attributes', async () => { @@ -899,6 +998,40 @@ describe('Task Runner', () => { "state": Object {}, } `); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(1); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "error": Object { + "message": "OMG", + }, + "event": Object { + "action": "execute", + "outcome": "failure", + "reason": "read", + }, + "kibana": Object { + "alerting": Object { + "status": "error", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + }, + ], + }, + "message": "test:1: execution failed", + }, + ], + ] + `); }); test('recovers gracefully when the Runner of a legacy Alert task which has no schedule throws an exception when fetching attributes', async () => { diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index 86bf7006e8d09e..5bccf5c497a60c 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { Dictionary, pickBy, mapValues, without } from 'lodash'; +import { Dictionary, pickBy, mapValues, without, cloneDeep } from 'lodash'; import { Logger, KibanaRequest } from '../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; import { ConcreteTaskInstance, throwUnrecoverableError } from '../../../task_manager/server'; @@ -40,6 +40,8 @@ import { partiallyUpdateAlert } from '../saved_objects'; const FALLBACK_RETRY_INTERVAL = '5m'; +type Event = Exclude; + interface AlertTaskRunResult { state: AlertTaskState; schedule: IntervalSchedule | undefined; @@ -153,7 +155,8 @@ export class TaskRunner { alert: SanitizedAlert, params: AlertExecutorOptions['params'], executionHandler: ReturnType, - spaceId: string + spaceId: string, + event: Event ): Promise { const { throttle, muteAll, mutedInstanceIds, name, tags, createdBy, updatedBy } = alert; const { @@ -166,24 +169,10 @@ export class TaskRunner { alertRawInstances, (rawAlertInstance) => new AlertInstance(rawAlertInstance) ); + const originalAlertInstances = cloneDeep(alertInstances); - const originalAlertInstanceIds = Object.keys(alertInstances); const eventLogger = this.context.eventLogger; const alertLabel = `${this.alertType.id}:${alertId}: '${name}'`; - const event: IEvent = { - event: { action: EVENT_LOG_ACTIONS.execute }, - kibana: { - saved_objects: [ - { - rel: SAVED_OBJECT_REL_PRIMARY, - type: 'alert', - id: alertId, - namespace, - }, - ], - }, - }; - eventLogger.startTiming(event); let updatedAlertTypeState: void | Record; try { @@ -205,21 +194,17 @@ export class TaskRunner { updatedBy, }); } catch (err) { - eventLogger.stopTiming(event); event.message = `alert execution failure: ${alertLabel}`; event.error = event.error || {}; event.error.message = err.message; event.event = event.event || {}; event.event.outcome = 'failure'; - eventLogger.logEvent(event); throw new ErrorWithReason(AlertExecutionStatusErrorReasons.Execute, err); } - eventLogger.stopTiming(event); event.message = `alert executed: ${alertLabel}`; event.event = event.event || {}; event.event.outcome = 'success'; - eventLogger.logEvent(event); // Cleanup alert instances that are no longer scheduling actions to avoid over populating the alertInstances object const instancesWithScheduledActions = pickBy(alertInstances, (alertInstance: AlertInstance) => @@ -227,7 +212,7 @@ export class TaskRunner { ); generateNewAndResolvedInstanceEvents({ eventLogger, - originalAlertInstanceIds, + originalAlertInstances, currentAlertInstances: instancesWithScheduledActions, alertId, alertLabel, @@ -261,7 +246,8 @@ export class TaskRunner { async validateAndExecuteAlert( services: Services, apiKey: RawAlert['apiKey'], - alert: SanitizedAlert + alert: SanitizedAlert, + event: Event ) { const { params: { alertId, spaceId }, @@ -278,10 +264,17 @@ export class TaskRunner { alert.actions, alert.params ); - return this.executeAlertInstances(services, alert, validatedParams, executionHandler, spaceId); + return this.executeAlertInstances( + services, + alert, + validatedParams, + executionHandler, + spaceId, + event + ); } - async loadAlertAttributesAndRun(): Promise> { + async loadAlertAttributesAndRun(event: Event): Promise> { const { params: { alertId, spaceId }, } = this.taskInstance; @@ -304,7 +297,7 @@ export class TaskRunner { return { state: await promiseResult( - this.validateAndExecuteAlert(services, apiKey, alert) + this.validateAndExecuteAlert(services, apiKey, alert, event) ), schedule: asOk( // fetch the alert again to ensure we return the correct schedule as it may have @@ -322,18 +315,65 @@ export class TaskRunner { schedule: taskSchedule, } = this.taskInstance; - const { state, schedule } = await errorAsAlertTaskRunResult(this.loadAlertAttributesAndRun()); - const namespace = spaceId === 'default' ? undefined : spaceId; + const namespace = this.context.spaceIdToNamespace(spaceId); + const eventLogger = this.context.eventLogger; + const event: IEvent = { + // explicitly set execute timestamp so it will be before other events + // generated here (new-instance, schedule-action, etc) + '@timestamp': new Date().toISOString(), + event: { action: EVENT_LOG_ACTIONS.execute }, + kibana: { + saved_objects: [ + { + rel: SAVED_OBJECT_REL_PRIMARY, + type: 'alert', + id: alertId, + namespace, + }, + ], + }, + }; + eventLogger.startTiming(event); + + const { state, schedule } = await errorAsAlertTaskRunResult( + this.loadAlertAttributesAndRun(event) + ); const executionStatus: AlertExecutionStatus = map( state, (alertTaskState: AlertTaskState) => executionStatusFromState(alertTaskState), (err: Error) => executionStatusFromError(err) ); + + // set the executionStatus date to same as event, if it's set + if (event.event?.start) { + executionStatus.lastExecutionDate = new Date(event.event.start); + } + this.logger.debug( `alertExecutionStatus for ${this.alertType.id}:${alertId}: ${JSON.stringify(executionStatus)}` ); + eventLogger.stopTiming(event); + event.kibana = event.kibana || {}; + event.kibana.alerting = event.kibana.alerting || {}; + event.kibana.alerting.status = executionStatus.status; + + // if executionStatus indicates an error, fill in fields in + // event from it + if (executionStatus.error) { + event.event = event.event || {}; + event.event.reason = executionStatus.error?.reason || 'unknown'; + event.event.outcome = 'failure'; + event.error = event.error || {}; + event.error.message = event.error.message || executionStatus.error.message; + if (!event.message) { + event.message = `${this.alertType.id}:${alertId}: execution failed`; + } + } + + eventLogger.logEvent(event); + const client = this.context.internalSavedObjectsRepository; const attributes = { executionStatus: alertExecutionStatusToRaw(executionStatus), @@ -381,7 +421,7 @@ export class TaskRunner { interface GenerateNewAndResolvedInstanceEventsParams { eventLogger: IEventLogger; - originalAlertInstanceIds: string[]; + originalAlertInstances: Dictionary; currentAlertInstances: Dictionary; alertId: string; alertLabel: string; @@ -389,26 +429,23 @@ interface GenerateNewAndResolvedInstanceEventsParams { } function generateNewAndResolvedInstanceEvents(params: GenerateNewAndResolvedInstanceEventsParams) { - const { - eventLogger, - alertId, - namespace, - currentAlertInstances, - originalAlertInstanceIds, - } = params; + const { eventLogger, alertId, namespace, currentAlertInstances, originalAlertInstances } = params; + const originalAlertInstanceIds = Object.keys(originalAlertInstances); const currentAlertInstanceIds = Object.keys(currentAlertInstances); const newIds = without(currentAlertInstanceIds, ...originalAlertInstanceIds); const resolvedIds = without(originalAlertInstanceIds, ...currentAlertInstanceIds); for (const id of resolvedIds) { + const actionGroup = originalAlertInstances[id].getLastScheduledActions()?.group; const message = `${params.alertLabel} resolved instance: '${id}'`; - logInstanceEvent(id, EVENT_LOG_ACTIONS.resolvedInstance, message); + logInstanceEvent(id, EVENT_LOG_ACTIONS.resolvedInstance, message, actionGroup); } for (const id of newIds) { + const actionGroup = currentAlertInstances[id].getScheduledActionOptions()?.actionGroup; const message = `${params.alertLabel} created new instance: '${id}'`; - logInstanceEvent(id, EVENT_LOG_ACTIONS.newInstance, message); + logInstanceEvent(id, EVENT_LOG_ACTIONS.newInstance, message, actionGroup); } for (const id of currentAlertInstanceIds) { @@ -425,7 +462,7 @@ function generateNewAndResolvedInstanceEvents(params: GenerateNewAndResolvedInst kibana: { alerting: { instance_id: instanceId, - action_group_id: group, + ...(group ? { action_group_id: group } : {}), }, saved_objects: [ { diff --git a/x-pack/plugins/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json index 5c7eb50117d9b1..3131235ebcfbeb 100644 --- a/x-pack/plugins/event_log/generated/mappings.json +++ b/x-pack/plugins/event_log/generated/mappings.json @@ -45,6 +45,10 @@ "outcome": { "ignore_above": 1024, "type": "keyword" + }, + "reason": { + "ignore_above": 1024, + "type": "keyword" } } }, @@ -85,6 +89,10 @@ "action_group_id": { "type": "keyword", "ignore_above": 1024 + }, + "status": { + "type": "keyword", + "ignore_above": 1024 } } }, diff --git a/x-pack/plugins/event_log/generated/schemas.ts b/x-pack/plugins/event_log/generated/schemas.ts index 3dbb43b15350f6..d2024ea8ed14a3 100644 --- a/x-pack/plugins/event_log/generated/schemas.ts +++ b/x-pack/plugins/event_log/generated/schemas.ts @@ -18,7 +18,7 @@ type DeepPartial = { [P in keyof T]?: T[P] extends Array ? Array> : DeepPartial; }; -export const ECS_VERSION = '1.5.0'; +export const ECS_VERSION = '1.6.0'; // types and config-schema describing the es structures export type IValidatedEvent = TypeOf; @@ -42,6 +42,7 @@ export const EventSchema = schema.maybe( duration: ecsNumber(), end: ecsDate(), outcome: ecsString(), + reason: ecsString(), }) ), error: schema.maybe( @@ -61,6 +62,7 @@ export const EventSchema = schema.maybe( schema.object({ instance_id: ecsString(), action_group_id: ecsString(), + status: ecsString(), }) ), saved_objects: schema.maybe( diff --git a/x-pack/plugins/event_log/scripts/mappings.js b/x-pack/plugins/event_log/scripts/mappings.js index c9af2b0aa57fb4..bd05f84d4e2b84 100644 --- a/x-pack/plugins/event_log/scripts/mappings.js +++ b/x-pack/plugins/event_log/scripts/mappings.js @@ -22,6 +22,10 @@ exports.EcsKibanaExtensionsMappings = { type: 'keyword', ignore_above: 1024, }, + status: { + type: 'keyword', + ignore_above: 1024, + }, }, }, // array of saved object references, for "linking" via search @@ -63,11 +67,13 @@ exports.EcsEventLogProperties = [ 'event.duration', 'event.end', 'event.outcome', // optional, but one of failure, success, unknown + 'event.reason', 'error.message', 'user.name', 'kibana.server_uuid', 'kibana.alerting.instance_id', 'kibana.alerting.action_group_id', + 'kibana.alerting.status', 'kibana.saved_objects.rel', 'kibana.saved_objects.namespace', 'kibana.saved_objects.id', diff --git a/x-pack/plugins/event_log/server/event_logger.test.ts b/x-pack/plugins/event_log/server/event_logger.test.ts index 0ab3071f70efaa..ea699af45ccd2f 100644 --- a/x-pack/plugins/event_log/server/event_logger.test.ts +++ b/x-pack/plugins/event_log/server/event_logger.test.ts @@ -102,16 +102,16 @@ describe('EventLogger', () => { event: { provider: 'test-provider', action: 'a' }, }); - const ignoredTimestamp = '1999-01-01T00:00:00Z'; + const respectedTimestamp = '2999-01-01T00:00:00.000Z'; eventLogger.logEvent({ - '@timestamp': ignoredTimestamp, + '@timestamp': respectedTimestamp, event: { action: 'b', }, }); const event = await waitForLogEvent(systemLogger); - expect(event!['@timestamp']).not.toEqual(ignoredTimestamp); + expect(event!['@timestamp']).toEqual(respectedTimestamp); expect(event?.event?.action).toEqual('b'); }); diff --git a/x-pack/plugins/event_log/server/event_logger.ts b/x-pack/plugins/event_log/server/event_logger.ts index 8730870f9620b4..658d90d8096525 100644 --- a/x-pack/plugins/event_log/server/event_logger.ts +++ b/x-pack/plugins/event_log/server/event_logger.ts @@ -72,7 +72,6 @@ export class EventLogger implements IEventLogger { const event: IEvent = {}; const fixedProperties = { - '@timestamp': new Date().toISOString(), ecs: { version: ECS_VERSION, }, @@ -81,8 +80,12 @@ export class EventLogger implements IEventLogger { }, }; + const defaultProperties = { + '@timestamp': new Date().toISOString(), + }; + // merge the initial properties and event properties - merge(event, this.initialProperties, eventProperties, fixedProperties); + merge(event, defaultProperties, this.initialProperties, eventProperties, fixedProperties); let validatedEvent: IValidatedEvent; try { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts new file mode 100644 index 00000000000000..385d8bfca4a9a3 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts @@ -0,0 +1,84 @@ +/* + * 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 { Spaces } from '../../scenarios'; +import { getUrlPrefix, getTestAlertData, ObjectRemover, getEventLog } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { validateEvent } from '../../../spaces_only/tests/alerting/event_log'; + +// eslint-disable-next-line import/no-default-export +export default function eventLogTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const retry = getService('retry'); + + describe('eventLog', () => { + const objectRemover = new ObjectRemover(supertest); + + after(() => objectRemover.removeAll()); + + it('should generate events for alert decrypt errors', async () => { + const spaceId = Spaces[0].id; + const response = await supertest + .post(`${getUrlPrefix(spaceId)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.noop', + schedule: { interval: '1s' }, + throttle: null, + }) + ); + + expect(response.status).to.eql(200); + const alertId = response.body.id; + objectRemover.add(spaceId, alertId, 'alert', 'alerts'); + + // break AAD + await supertest + .put(`${getUrlPrefix(spaceId)}/api/alerts_fixture/saved_object/alert/${alertId}`) + .set('kbn-xsrf', 'foo') + .send({ + attributes: { + name: 'bar', + }, + }) + .expect(200); + + const events = await retry.try(async () => { + // there can be a successful execute before the error one + const someEvents = await getEventLog({ + getService, + spaceId, + type: 'alert', + id: alertId, + provider: 'alerting', + actions: ['execute'], + }); + const errorEvents = someEvents.filter( + (event) => event?.kibana?.alerting?.status === 'error' + ); + if (errorEvents.length === 0) { + throw new Error('no execute/error events yet'); + } + return errorEvents; + }); + + const event = events[0]; + expect(event).to.be.ok(); + + validateEvent(event, { + spaceId, + savedObjects: [{ type: 'alert', id: alertId, rel: 'primary' }], + outcome: 'failure', + message: `test.noop:${alertId}: execution failed`, + errorMessage: 'Unable to decrypt attribute "apiKey"', + status: 'error', + reason: 'decrypt', + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts index 1fbee9e18fdaa2..4f8525cfcf683c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts @@ -26,6 +26,7 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./update')); loadTestFile(require.resolve('./update_api_key')); loadTestFile(require.resolve('./alerts')); + loadTestFile(require.resolve('./event_log')); // note that this test will destroy existing spaces loadTestFile(require.resolve('./rbac_legacy')); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts index dbf8eb162fca71..937045b6218c6d 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts @@ -107,6 +107,8 @@ export default function eventLogTests({ getService }: FtrProviderContext) { expect(resolvedInstanceTimes[0] > newInstanceTimes[0]).to.be(true); // validate each event + let executeCount = 0; + const executeStatuses = ['ok', 'active', 'active']; for (const event of events) { switch (event?.event?.action) { case 'execute': @@ -115,6 +117,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { savedObjects: [{ type: 'alert', id: alertId, rel: 'primary' }], outcome: 'success', message: `alert executed: test.patternFiring:${alertId}: 'abc'`, + status: executeStatuses[executeCount++], }); break; case 'execute-action': @@ -125,6 +128,8 @@ export default function eventLogTests({ getService }: FtrProviderContext) { { type: 'action', id: createdAction.id }, ], message: `alert: test.patternFiring:${alertId}: 'abc' instanceId: 'instance' scheduled actionGroup: 'default' action: test.noop:${createdAction.id}`, + instanceId: 'instance', + actionGroupId: 'default', }); break; case 'new-instance': @@ -147,6 +152,8 @@ export default function eventLogTests({ getService }: FtrProviderContext) { spaceId: Spaces.space1.id, savedObjects: [{ type: 'alert', id: alertId, rel: 'primary' }], message: `test.patternFiring:${alertId}: 'abc' ${subMessage}`, + instanceId: 'instance', + actionGroupId: 'default', }); } }); @@ -187,60 +194,83 @@ export default function eventLogTests({ getService }: FtrProviderContext) { outcome: 'failure', message: `alert execution failure: test.throw:${alertId}: 'abc'`, errorMessage: 'this alert is intended to fail', + status: 'error', + reason: 'execute', }); }); }); +} + +interface SavedObject { + type: string; + id: string; + rel?: string; +} + +interface ValidateEventLogParams { + spaceId: string; + savedObjects: SavedObject[]; + outcome?: string; + message: string; + errorMessage?: string; + status?: string; + actionGroupId?: string; + instanceId?: string; + reason?: string; +} + +export function validateEvent(event: IValidatedEvent, params: ValidateEventLogParams): void { + const { spaceId, savedObjects, outcome, message, errorMessage } = params; + const { status, actionGroupId, instanceId, reason } = params; - interface SavedObject { - type: string; - id: string; - rel?: string; + if (status) { + expect(event?.kibana?.alerting?.status).to.be(status); } - interface ValidateEventLogParams { - spaceId: string; - savedObjects: SavedObject[]; - outcome?: string; - message: string; - errorMessage?: string; + if (actionGroupId) { + expect(event?.kibana?.alerting?.action_group_id).to.be(actionGroupId); } - function validateEvent(event: IValidatedEvent, params: ValidateEventLogParams): void { - const { spaceId, savedObjects, outcome, message, errorMessage } = params; + if (instanceId) { + expect(event?.kibana?.alerting?.instance_id).to.be(instanceId); + } - const duration = event?.event?.duration; - const eventStart = Date.parse(event?.event?.start || 'undefined'); - const eventEnd = Date.parse(event?.event?.end || 'undefined'); - const dateNow = Date.now(); + if (reason) { + expect(event?.event?.reason).to.be(reason); + } - if (duration !== undefined) { - expect(typeof duration).to.be('number'); - expect(eventStart).to.be.ok(); - expect(eventEnd).to.be.ok(); + const duration = event?.event?.duration; + const eventStart = Date.parse(event?.event?.start || 'undefined'); + const eventEnd = Date.parse(event?.event?.end || 'undefined'); + const dateNow = Date.now(); - const durationDiff = Math.abs( - Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart) - ); + if (duration !== undefined) { + expect(typeof duration).to.be('number'); + expect(eventStart).to.be.ok(); + expect(eventEnd).to.be.ok(); - // account for rounding errors - expect(durationDiff < 1).to.equal(true); - expect(eventStart <= eventEnd).to.equal(true); - expect(eventEnd <= dateNow).to.equal(true); - } + const durationDiff = Math.abs( + Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart) + ); - expect(event?.event?.outcome).to.equal(outcome); + // account for rounding errors + expect(durationDiff < 1).to.equal(true); + expect(eventStart <= eventEnd).to.equal(true); + expect(eventEnd <= dateNow).to.equal(true); + } - for (const savedObject of savedObjects) { - expect( - isSavedObjectInEvent(event, spaceId, savedObject.type, savedObject.id, savedObject.rel) - ).to.be(true); - } + expect(event?.event?.outcome).to.equal(outcome); - expect(event?.message).to.eql(message); + for (const savedObject of savedObjects) { + expect( + isSavedObjectInEvent(event, spaceId, savedObject.type, savedObject.id, savedObject.rel) + ).to.be(true); + } - if (errorMessage) { - expect(event?.error?.message).to.eql(errorMessage); - } + expect(event?.message).to.eql(message); + + if (errorMessage) { + expect(event?.error?.message).to.eql(errorMessage); } } From 55519665d64d58dd5bcc4773e609715c6b951cb7 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Thu, 12 Nov 2020 14:38:07 +0000 Subject: [PATCH 21/52] [Advanced Settings] Introducing telemetry (#82860) * [Advaned Settings] Introducing telemetry * Publishing doc changes * Move metric tracking to onSave method * Adding deprecated warning * Updating docs Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...ana-plugin-core-public.uisettingsparams.md | 1 + ...gin-core-public.uisettingsparams.metric.md | 21 +++++++++++++++++++ ...ana-plugin-core-server.uisettingsparams.md | 1 + ...gin-core-server.uisettingsparams.metric.md | 21 +++++++++++++++++++ src/core/public/public.api.md | 6 ++++++ src/core/server/server.api.md | 6 ++++++ src/core/types/ui_settings.ts | 10 +++++++++ src/plugins/advanced_settings/kibana.json | 2 +- .../management_app/advanced_settings.tsx | 4 ++++ .../management_app/components/form/form.tsx | 9 +++++++- .../management_app/lib/to_editable_config.ts | 1 + .../mount_management_section.tsx | 6 +++++- .../public/management_app/types.ts | 5 +++++ .../advanced_settings/public/plugin.ts | 12 +++++++++-- src/plugins/advanced_settings/public/types.ts | 2 ++ src/plugins/data/server/server.api.md | 1 + src/plugins/discover/server/ui_settings.ts | 7 ++++++- 17 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.uisettingsparams.metric.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.uisettingsparams.metric.md diff --git a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md index e7facb4a109cd8..4a9fc940c596ff 100644 --- a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md +++ b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md @@ -19,6 +19,7 @@ export interface UiSettingsParams | [category](./kibana-plugin-core-public.uisettingsparams.category.md) | string[] | used to group the configured setting in the UI | | [deprecation](./kibana-plugin-core-public.uisettingsparams.deprecation.md) | DeprecationSettings | optional deprecation information. Used to generate a deprecation warning. | | [description](./kibana-plugin-core-public.uisettingsparams.description.md) | string | description provided to a user in UI | +| [metric](./kibana-plugin-core-public.uisettingsparams.metric.md) | {
type: UiStatsMetricType;
name: string;
} | Metric to track once this property changes | | [name](./kibana-plugin-core-public.uisettingsparams.name.md) | string | title in the UI | | [optionLabels](./kibana-plugin-core-public.uisettingsparams.optionlabels.md) | Record<string, string> | text labels for 'select' type UI element | | [options](./kibana-plugin-core-public.uisettingsparams.options.md) | string[] | array of permitted values for this setting | diff --git a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.metric.md b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.metric.md new file mode 100644 index 00000000000000..0855cfd77a46b4 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.metric.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) > [metric](./kibana-plugin-core-public.uisettingsparams.metric.md) + +## UiSettingsParams.metric property + +> Warning: This API is now obsolete. +> +> Temporary measure until https://github.com/elastic/kibana/issues/83084 is in place +> + +Metric to track once this property changes + +Signature: + +```typescript +metric?: { + type: UiStatsMetricType; + name: string; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md index f134decb5102bd..7bcb996e98e167 100644 --- a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md +++ b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md @@ -19,6 +19,7 @@ export interface UiSettingsParams | [category](./kibana-plugin-core-server.uisettingsparams.category.md) | string[] | used to group the configured setting in the UI | | [deprecation](./kibana-plugin-core-server.uisettingsparams.deprecation.md) | DeprecationSettings | optional deprecation information. Used to generate a deprecation warning. | | [description](./kibana-plugin-core-server.uisettingsparams.description.md) | string | description provided to a user in UI | +| [metric](./kibana-plugin-core-server.uisettingsparams.metric.md) | {
type: UiStatsMetricType;
name: string;
} | Metric to track once this property changes | | [name](./kibana-plugin-core-server.uisettingsparams.name.md) | string | title in the UI | | [optionLabels](./kibana-plugin-core-server.uisettingsparams.optionlabels.md) | Record<string, string> | text labels for 'select' type UI element | | [options](./kibana-plugin-core-server.uisettingsparams.options.md) | string[] | array of permitted values for this setting | diff --git a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.metric.md b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.metric.md new file mode 100644 index 00000000000000..4d54bf9ae472b8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.metric.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) > [metric](./kibana-plugin-core-server.uisettingsparams.metric.md) + +## UiSettingsParams.metric property + +> Warning: This API is now obsolete. +> +> Temporary measure until https://github.com/elastic/kibana/issues/83084 is in place +> + +Metric to track once this property changes + +Signature: + +```typescript +metric?: { + type: UiStatsMetricType; + name: string; + }; +``` diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 781a50f849e241..c8add5a8ddf581 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -38,6 +38,7 @@ import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; +import { UiStatsMetricType } from '@kbn/analytics'; import { UnregisterCallback } from 'history'; import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/types'; @@ -1362,6 +1363,11 @@ export interface UiSettingsParams { // Warning: (ae-forgotten-export) The symbol "DeprecationSettings" needs to be exported by the entry point index.d.ts deprecation?: DeprecationSettings; description?: string; + // @deprecated + metric?: { + type: UiStatsMetricType; + name: string; + }; name?: string; optionLabels?: Record; options?: string[]; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 88d7fecbcf502f..a03e5ec9acd275 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -160,6 +160,7 @@ import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; +import { UiStatsMetricType } from '@kbn/analytics'; import { UpdateDocumentByQueryParams } from 'elasticsearch'; import { UpdateDocumentParams } from 'elasticsearch'; import { URL } from 'url'; @@ -2746,6 +2747,11 @@ export interface UiSettingsParams { category?: string[]; deprecation?: DeprecationSettings; description?: string; + // @deprecated + metric?: { + type: UiStatsMetricType; + name: string; + }; name?: string; optionLabels?: Record; options?: string[]; diff --git a/src/core/types/ui_settings.ts b/src/core/types/ui_settings.ts index ed1076b5719603..0b7a8e1efd9df7 100644 --- a/src/core/types/ui_settings.ts +++ b/src/core/types/ui_settings.ts @@ -17,6 +17,7 @@ * under the License. */ import { Type } from '@kbn/config-schema'; +import { UiStatsMetricType } from '@kbn/analytics'; /** * UI element type to represent the settings. @@ -80,6 +81,15 @@ export interface UiSettingsParams { * Used to validate value on write and read. */ schema: Type; + /** + * Metric to track once this property changes + * @deprecated + * Temporary measure until https://github.com/elastic/kibana/issues/83084 is in place + */ + metric?: { + type: UiStatsMetricType; + name: string; + }; } /** diff --git a/src/plugins/advanced_settings/kibana.json b/src/plugins/advanced_settings/kibana.json index 0e49fe17089f0b..df0d31a904c599 100644 --- a/src/plugins/advanced_settings/kibana.json +++ b/src/plugins/advanced_settings/kibana.json @@ -4,6 +4,6 @@ "server": true, "ui": true, "requiredPlugins": ["management"], - "optionalPlugins": ["home"], + "optionalPlugins": ["home", "usageCollection"], "requiredBundles": ["kibanaReact", "home"] } diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx index afdd90959eabde..bbc27ca025ede6 100644 --- a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx @@ -22,6 +22,7 @@ import { Subscription } from 'rxjs'; import { Comparators, EuiFlexGroup, EuiFlexItem, EuiSpacer, Query } from '@elastic/eui'; import { useParams } from 'react-router-dom'; +import { UiStatsMetricType } from '@kbn/analytics'; import { CallOuts } from './components/call_outs'; import { Search } from './components/search'; import { Form } from './components/form'; @@ -39,6 +40,7 @@ interface AdvancedSettingsProps { dockLinks: DocLinksStart['links']; toasts: ToastsStart; componentRegistry: ComponentRegistry['start']; + trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void; } interface AdvancedSettingsComponentProps extends AdvancedSettingsProps { @@ -241,6 +243,7 @@ export class AdvancedSettingsComponent extends Component< enableSaving={this.props.enableSaving} dockLinks={this.props.dockLinks} toasts={this.props.toasts} + trackUiMetric={this.props.trackUiMetric} /> { dockLinks={props.dockLinks} toasts={props.toasts} componentRegistry={props.componentRegistry} + trackUiMetric={props.trackUiMetric} /> ); }; diff --git a/src/plugins/advanced_settings/public/management_app/components/form/form.tsx b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx index d243d85e12a665..c30768a262056e 100644 --- a/src/plugins/advanced_settings/public/management_app/components/form/form.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx @@ -36,6 +36,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { isEmpty } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { UiStatsMetricType } from '@kbn/analytics'; import { toMountPoint } from '../../../../../kibana_react/public'; import { DocLinksStart, ToastsStart } from '../../../../../../core/public'; @@ -56,6 +57,7 @@ interface FormProps { enableSaving: boolean; dockLinks: DocLinksStart['links']; toasts: ToastsStart; + trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void; } interface FormState { @@ -149,7 +151,7 @@ export class Form extends PureComponent { if (!setting) { return; } - const { defVal, type, requiresPageReload } = setting; + const { defVal, type, requiresPageReload, metric } = setting; let valueToSave = value; let equalsToDefault = false; switch (type) { @@ -163,6 +165,11 @@ export class Form extends PureComponent { const isArray = Array.isArray(JSON.parse((defVal as string) || '{}')); valueToSave = valueToSave.trim(); valueToSave = valueToSave || (isArray ? '[]' : '{}'); + case 'boolean': + if (metric && this.props.trackUiMetric) { + const metricName = valueToSave ? `${metric.name}_on` : `${metric.name}_off`; + this.props.trackUiMetric(metric.type, metricName); + } default: equalsToDefault = valueToSave === defVal; } diff --git a/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts b/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts index 406bc35f826e84..e5a1ee1437a91a 100644 --- a/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts +++ b/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts @@ -75,6 +75,7 @@ export function toEditableConfig({ options: def.options, optionLabels: def.optionLabels, requiresPageReload: !!def.requiresPageReload, + metric: def.metric, }; return conf; diff --git a/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx b/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx index ab348451b1eef8..0b3d73cb28806f 100644 --- a/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx +++ b/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx @@ -30,6 +30,7 @@ import { ManagementAppMountParams } from '../../../management/public'; import { ComponentRegistry } from '../types'; import './index.scss'; +import { UsageCollectionSetup } from '../../../usage_collection/public'; const title = i18n.translate('advancedSettings.advancedSettingsLabel', { defaultMessage: 'Advanced Settings', @@ -49,12 +50,14 @@ const readOnlyBadge = { export async function mountManagementSection( getStartServices: StartServicesAccessor, params: ManagementAppMountParams, - componentRegistry: ComponentRegistry['start'] + componentRegistry: ComponentRegistry['start'], + usageCollection?: UsageCollectionSetup ) { params.setBreadcrumbs(crumb); const [{ uiSettings, notifications, docLinks, application, chrome }] = await getStartServices(); const canSave = application.capabilities.advancedSettings.save as boolean; + const trackUiMetric = usageCollection?.reportUiStats.bind(usageCollection, 'advanced_settings'); if (!canSave) { chrome.setBadge(readOnlyBadge); @@ -71,6 +74,7 @@ export async function mountManagementSection( dockLinks={docLinks.links} uiSettings={uiSettings} componentRegistry={componentRegistry} + trackUiMetric={trackUiMetric} /> diff --git a/src/plugins/advanced_settings/public/management_app/types.ts b/src/plugins/advanced_settings/public/management_app/types.ts index 6e243926f7d7dc..05e695f9985000 100644 --- a/src/plugins/advanced_settings/public/management_app/types.ts +++ b/src/plugins/advanced_settings/public/management_app/types.ts @@ -17,6 +17,7 @@ * under the License. */ +import { UiStatsMetricType } from '@kbn/analytics'; import { UiSettingsType, StringValidation, ImageValidation } from '../../../../core/public'; export interface FieldSetting { @@ -39,6 +40,10 @@ export interface FieldSetting { message: string; docLinksKey: string; }; + metric?: { + type: UiStatsMetricType; + name: string; + }; } // until eui searchbar and query are typed diff --git a/src/plugins/advanced_settings/public/plugin.ts b/src/plugins/advanced_settings/public/plugin.ts index 188b11177eaec6..165af48b2023ca 100644 --- a/src/plugins/advanced_settings/public/plugin.ts +++ b/src/plugins/advanced_settings/public/plugin.ts @@ -30,7 +30,10 @@ const title = i18n.translate('advancedSettings.advancedSettingsLabel', { export class AdvancedSettingsPlugin implements Plugin { - public setup(core: CoreSetup, { management, home }: AdvancedSettingsPluginSetup) { + public setup( + core: CoreSetup, + { management, home, usageCollection }: AdvancedSettingsPluginSetup + ) { const kibanaSection = management.sections.section.kibana; kibanaSection.registerApp({ @@ -41,7 +44,12 @@ export class AdvancedSettingsPlugin const { mountManagementSection } = await import( './management_app/mount_management_section' ); - return mountManagementSection(core.getStartServices, params, component.start); + return mountManagementSection( + core.getStartServices, + params, + component.start, + usageCollection + ); }, }); diff --git a/src/plugins/advanced_settings/public/types.ts b/src/plugins/advanced_settings/public/types.ts index cc59f52b1f30fb..bd5cb0e61fb042 100644 --- a/src/plugins/advanced_settings/public/types.ts +++ b/src/plugins/advanced_settings/public/types.ts @@ -21,6 +21,7 @@ import { ComponentRegistry } from './component_registry'; import { HomePublicPluginSetup } from '../../home/public'; import { ManagementSetup } from '../../management/public'; +import { UsageCollectionSetup } from '../../usage_collection/public'; export interface AdvancedSettingsSetup { component: ComponentRegistry['setup']; @@ -32,6 +33,7 @@ export interface AdvancedSettingsStart { export interface AdvancedSettingsPluginSetup { management: ManagementSetup; home?: HomePublicPluginSetup; + usageCollection?: UsageCollectionSetup; } export { ComponentRegistry }; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 2984ca336819ae..bb7a8f58c926ce 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -60,6 +60,7 @@ import { ShardsResponse } from 'elasticsearch'; import { ToastInputFields } from 'src/core/public/notifications'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; +import { UiStatsMetricType } from '@kbn/analytics'; import { Unit } from '@elastic/datemath'; // Warning: (ae-forgotten-export) The symbol "AggConfigSerialized" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts index 5447b982eef146..f45281ee62202c 100644 --- a/src/plugins/discover/server/ui_settings.ts +++ b/src/plugins/discover/server/ui_settings.ts @@ -21,6 +21,7 @@ import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; import { UiSettingsParams } from 'kibana/server'; +import { METRIC_TYPE } from '@kbn/analytics'; import { DEFAULT_COLUMNS_SETTING, SAMPLE_SIZE_SETTING, @@ -170,9 +171,13 @@ export const uiSettings: Record = { }), value: true, description: i18n.translate('discover.advancedSettings.discover.modifyColumnsOnSwitchText', { - defaultMessage: 'Remove columns that not available in the new index pattern.', + defaultMessage: 'Remove columns that are not available in the new index pattern.', }), category: ['discover'], schema: schema.boolean(), + metric: { + type: METRIC_TYPE.CLICK, + name: 'discover:modifyColumnsOnSwitchTitle', + }, }, }; From afbf1a983aadd60621ccc3689876211688f3fa1d Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 12 Nov 2020 15:49:22 +0100 Subject: [PATCH 22/52] [APM] Errors table for service overview (#83065) --- .../runtime_types/to_number_rt/index.ts | 16 ++ .../ServiceList/ServiceListMetric.tsx | 33 +-- .../components/app/service_overview/index.tsx | 34 +-- .../fetch_wrapper.tsx | 30 ++ .../service_overview_errors_table/index.tsx | 266 ++++++++++++++++++ .../service_overview/table_link_flex_item.tsx | 14 + .../{SparkPlot => spark_plot}/index.tsx | 0 .../spark_plot_with_value_label/index.tsx | 57 ++++ .../apm/server/lib/errors/get_error_groups.ts | 4 +- .../apm/server/lib/helpers/get_error_name.ts | 11 + .../get_service_error_groups/index.ts | 177 ++++++++++++ .../apm/server/routes/create_apm_api.ts | 2 + x-pack/plugins/apm/server/routes/services.ts | 44 +++ .../monitoring/workload_statistics.test.ts | 32 ++- .../apm_api_integration/basic/tests/index.ts | 4 + .../tests/service_overview/error_groups.ts | 220 +++++++++++++++ .../typings/elasticsearch/aggregations.d.ts | 2 + 17 files changed, 883 insertions(+), 63 deletions(-) create mode 100644 x-pack/plugins/apm/common/runtime_types/to_number_rt/index.ts create mode 100644 x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx create mode 100644 x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx create mode 100644 x-pack/plugins/apm/public/components/app/service_overview/table_link_flex_item.tsx rename x-pack/plugins/apm/public/components/shared/charts/{SparkPlot => spark_plot}/index.tsx (100%) create mode 100644 x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx create mode 100644 x-pack/plugins/apm/server/lib/helpers/get_error_name.ts create mode 100644 x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts create mode 100644 x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts diff --git a/x-pack/plugins/apm/common/runtime_types/to_number_rt/index.ts b/x-pack/plugins/apm/common/runtime_types/to_number_rt/index.ts new file mode 100644 index 00000000000000..0fe8181c114052 --- /dev/null +++ b/x-pack/plugins/apm/common/runtime_types/to_number_rt/index.ts @@ -0,0 +1,16 @@ +/* + * 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 t from 'io-ts'; + +export const toNumberRt = new t.Type( + 'ToNumber', + t.any.is, + (input, context) => { + const number = Number(input); + return !isNaN(number) ? t.success(number) : t.failure(input, context); + }, + t.identity +); diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx index c94c94d4a0b72c..716fed7775f7b4 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx @@ -3,15 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { EuiFlexItem } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; - import React from 'react'; -import { useTheme } from '../../../../hooks/useTheme'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { getEmptySeries } from '../../../shared/charts/CustomPlot/getEmptySeries'; -import { SparkPlot } from '../../../shared/charts/SparkPlot'; +import { SparkPlotWithValueLabel } from '../../../shared/charts/spark_plot/spark_plot_with_value_label'; export function ServiceListMetric({ color, @@ -22,28 +16,17 @@ export function ServiceListMetric({ series?: Array<{ x: number; y: number | null }>; valueLabel: React.ReactNode; }) { - const theme = useTheme(); - const { urlParams: { start, end }, } = useUrlParams(); - const colorValue = theme.eui[color]; - return ( - - - - - - {valueLabel} - - + ); } diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index 016ee3daf6b510..ee77157fe4eb36 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -12,9 +12,10 @@ import { useTrackPageview } from '../../../../../observability/public'; import { isRumAgentName } from '../../../../common/agent_name'; import { ChartsSyncContextProvider } from '../../../context/charts_sync_context'; import { TransactionErrorRateChart } from '../../shared/charts/transaction_error_rate_chart'; -import { ErrorOverviewLink } from '../../shared/Links/apm/ErrorOverviewLink'; import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink'; import { TransactionOverviewLink } from '../../shared/Links/apm/TransactionOverviewLink'; +import { ServiceOverviewErrorsTable } from './service_overview_errors_table'; +import { TableLinkFlexItem } from './table_link_flex_item'; const rowHeight = 310; const latencyChartRowHeight = 230; @@ -27,12 +28,6 @@ const LatencyChartRow = styled(EuiFlexItem)` height: ${latencyChartRowHeight}px; `; -const TableLinkFlexItem = styled(EuiFlexItem)` - & > a { - text-align: right; - } -`; - interface ServiceOverviewProps { agentName?: string; serviceName: string; @@ -130,30 +125,7 @@ export function ServiceOverview({ )} - - - -

- {i18n.translate( - 'xpack.apm.serviceOverview.errorsTableTitle', - { - defaultMessage: 'Errors', - } - )} -

-
-
- - - {i18n.translate( - 'xpack.apm.serviceOverview.errorsTableLinkText', - { - defaultMessage: 'View errors', - } - )} - - -
+
diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx new file mode 100644 index 00000000000000..4c8d368811a0c1 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx @@ -0,0 +1,30 @@ +/* + * 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 { FETCH_STATUS } from '../../../../hooks/useFetcher'; +import { ErrorStatePrompt } from '../../../shared/ErrorStatePrompt'; +import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; + +export function FetchWrapper({ + hasData, + status, + children, +}: { + hasData: boolean; + status: FETCH_STATUS; + children: React.ReactNode; +}) { + if (status === FETCH_STATUS.FAILURE) { + return ; + } + + if (!hasData && status !== FETCH_STATUS.SUCCESS) { + return ; + } + + return <>{children}; +} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx new file mode 100644 index 00000000000000..a5a002cf3aca49 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx @@ -0,0 +1,266 @@ +/* + * 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 { EuiTitle } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiBasicTable } from '@elastic/eui'; +import { EuiBasicTableColumn } from '@elastic/eui'; +import styled from 'styled-components'; +import { EuiToolTip } from '@elastic/eui'; +import { asInteger } from '../../../../../common/utils/formatters'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/useFetcher'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink'; +import { TableLinkFlexItem } from '../table_link_flex_item'; +import { SparkPlotWithValueLabel } from '../../../shared/charts/spark_plot/spark_plot_with_value_label'; +import { callApmApi } from '../../../../services/rest/createCallApmApi'; +import { TimestampTooltip } from '../../../shared/TimestampTooltip'; +import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; +import { px, truncate, unit } from '../../../../style/variables'; +import { FetchWrapper } from './fetch_wrapper'; + +interface Props { + serviceName: string; +} + +interface ErrorGroupItem { + name: string; + last_seen: number; + group_id: string; + occurrences: { + value: number; + timeseries: Array<{ x: number; y: number }> | null; + }; +} + +type SortDirection = 'asc' | 'desc'; +type SortField = 'name' | 'last_seen' | 'occurrences'; + +const PAGE_SIZE = 5; +const DEFAULT_SORT = { + direction: 'desc' as const, + field: 'occurrences' as const, +}; + +const ErrorDetailLinkWrapper = styled.div` + width: 100%; + .euiToolTipAnchor { + width: 100% !important; + } +`; + +const StyledErrorDetailLink = styled(ErrorDetailLink)` + display: block; + ${truncate('100%')} +`; + +export function ServiceOverviewErrorsTable({ serviceName }: Props) { + const { + urlParams: { start, end }, + uiFilters, + } = useUrlParams(); + + const [tableOptions, setTableOptions] = useState<{ + pageIndex: number; + sort: { + direction: SortDirection; + field: SortField; + }; + }>({ + pageIndex: 0, + sort: DEFAULT_SORT, + }); + + const columns: Array> = [ + { + field: 'name', + name: i18n.translate('xpack.apm.serviceOverview.errorsTableColumnName', { + defaultMessage: 'Name', + }), + render: (_, { name, group_id: errorGroupId }) => { + return ( + + + + {name} + + + + ); + }, + }, + { + field: 'last_seen', + name: i18n.translate( + 'xpack.apm.serviceOverview.errorsTableColumnLastSeen', + { + defaultMessage: 'Last seen', + } + ), + render: (_, { last_seen: lastSeen }) => { + return ; + }, + width: px(unit * 8), + }, + { + field: 'occurrences', + name: i18n.translate( + 'xpack.apm.serviceOverview.errorsTableColumnOccurrences', + { + defaultMessage: 'Occurrences', + } + ), + width: px(unit * 12), + render: (_, { occurrences }) => { + return ( + + ); + }, + }, + ]; + + const { + data = { + totalItemCount: 0, + items: [], + tableOptions: { + pageIndex: 0, + sort: DEFAULT_SORT, + }, + }, + status, + } = useFetcher(() => { + if (!start || !end) { + return; + } + + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/error_groups', + params: { + path: { serviceName }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + size: PAGE_SIZE, + numBuckets: 20, + pageIndex: tableOptions.pageIndex, + sortField: tableOptions.sort.field, + sortDirection: tableOptions.sort.direction, + }, + }, + }).then((response) => { + return { + items: response.error_groups, + totalItemCount: response.total_error_groups, + tableOptions: { + pageIndex: tableOptions.pageIndex, + sort: { + field: tableOptions.sort.field, + direction: tableOptions.sort.direction, + }, + }, + }; + }); + }, [ + start, + end, + serviceName, + uiFilters, + tableOptions.pageIndex, + tableOptions.sort.field, + tableOptions.sort.direction, + ]); + + const { + items, + totalItemCount, + tableOptions: { pageIndex, sort }, + } = data; + + return ( + + + + + +

+ {i18n.translate('xpack.apm.serviceOverview.errorsTableTitle', { + defaultMessage: 'Errors', + })} +

+
+
+ + + {i18n.translate('xpack.apm.serviceOverview.errorsTableLinkText', { + defaultMessage: 'View errors', + })} + + +
+
+ + + { + setTableOptions({ + pageIndex: newTableOptions.page?.index ?? 0, + sort: newTableOptions.sort + ? { + field: newTableOptions.sort.field as SortField, + direction: newTableOptions.sort.direction, + } + : DEFAULT_SORT, + }); + }} + sorting={{ + enableAllColumns: true, + sort: { + direction: sort.direction, + field: sort.field, + }, + }} + /> + + +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/table_link_flex_item.tsx b/x-pack/plugins/apm/public/components/app/service_overview/table_link_flex_item.tsx new file mode 100644 index 00000000000000..35df003af380d4 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/table_link_flex_item.tsx @@ -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. + */ + +import { EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; + +export const TableLinkFlexItem = styled(EuiFlexItem)` + & > a { + text-align: right; + } +`; diff --git a/x-pack/plugins/apm/public/components/shared/charts/SparkPlot/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/charts/SparkPlot/index.tsx rename to x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx diff --git a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx new file mode 100644 index 00000000000000..e2bb42fddb33bb --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx @@ -0,0 +1,57 @@ +/* + * 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 { EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; + +import React from 'react'; +import { useTheme } from '../../../../../hooks/useTheme'; +import { getEmptySeries } from '../../CustomPlot/getEmptySeries'; +import { SparkPlot } from '../'; + +type Color = + | 'euiColorVis0' + | 'euiColorVis1' + | 'euiColorVis2' + | 'euiColorVis3' + | 'euiColorVis4' + | 'euiColorVis5' + | 'euiColorVis6' + | 'euiColorVis7' + | 'euiColorVis8' + | 'euiColorVis9'; + +export function SparkPlotWithValueLabel({ + start, + end, + color, + series, + valueLabel, +}: { + start: number; + end: number; + color: Color; + series?: Array<{ x: number; y: number | null }>; + valueLabel: React.ReactNode; +}) { + const theme = useTheme(); + + const colorValue = theme.eui[color]; + + return ( + + + + + + {valueLabel} + + + ); +} diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts index d734a1395fc5e4..97c03924538c80 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts @@ -16,6 +16,7 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { getErrorGroupsProjection } from '../../projections/errors'; import { mergeProjection } from '../../projections/util/merge_projection'; +import { getErrorName } from '../helpers/get_error_name'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; export type ErrorGroupListAPIResponse = PromiseReturnType< @@ -93,8 +94,7 @@ export async function getErrorGroups({ // this is an exception rather than the rule so the ES type does not account for this. const hits = (resp.aggregations?.error_groups.buckets || []).map((bucket) => { const source = bucket.sample.hits.hits[0]._source; - const message = - source.error.log?.message || source.error.exception?.[0]?.message; + const message = getErrorName(source); return { message, diff --git a/x-pack/plugins/apm/server/lib/helpers/get_error_name.ts b/x-pack/plugins/apm/server/lib/helpers/get_error_name.ts new file mode 100644 index 00000000000000..dbc69592a4f8e8 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/get_error_name.ts @@ -0,0 +1,11 @@ +/* + * 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 { APMError } from '../../../typings/es_schemas/ui/apm_error'; + +export function getErrorName({ error }: APMError) { + return error.log?.message || error.exception?.[0]?.message; +} diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts new file mode 100644 index 00000000000000..99d978116180b7 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts @@ -0,0 +1,177 @@ +/* + * 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 { ValuesType } from 'utility-types'; +import { orderBy } from 'lodash'; +import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; +import { PromiseReturnType } from '../../../../../observability/typings/common'; +import { rangeFilter } from '../../../../common/utils/range_filter'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { + ERROR_EXC_MESSAGE, + ERROR_GROUP_ID, + ERROR_LOG_MESSAGE, + SERVICE_NAME, +} from '../../../../common/elasticsearch_fieldnames'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { getErrorName } from '../../helpers/get_error_name'; + +export type ServiceErrorGroupItem = ValuesType< + PromiseReturnType +>; + +export async function getServiceErrorGroups({ + serviceName, + setup, + size, + numBuckets, + pageIndex, + sortDirection, + sortField, +}: { + serviceName: string; + setup: Setup & SetupTimeRange; + size: number; + pageIndex: number; + numBuckets: number; + sortDirection: 'asc' | 'desc'; + sortField: 'name' | 'last_seen' | 'occurrences'; +}) { + const { apmEventClient, start, end, esFilter } = setup; + + const { intervalString } = getBucketSize(start, end, numBuckets); + + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { range: rangeFilter(start, end) }, + ...esFilter, + ], + }, + }, + aggs: { + error_groups: { + terms: { + field: ERROR_GROUP_ID, + size: 500, + order: { + _count: 'desc', + }, + }, + aggs: { + sample: { + top_hits: { + size: 1, + _source: [ERROR_LOG_MESSAGE, ERROR_EXC_MESSAGE, '@timestamp'], + sort: { + '@timestamp': 'desc', + }, + }, + }, + }, + }, + }, + }, + }); + + const errorGroups = + response.aggregations?.error_groups.buckets.map((bucket) => ({ + group_id: bucket.key as string, + name: + getErrorName(bucket.sample.hits.hits[0]._source) ?? NOT_AVAILABLE_LABEL, + last_seen: new Date( + bucket.sample.hits.hits[0]?._source['@timestamp'] + ).getTime(), + occurrences: { + value: bucket.doc_count, + }, + })) ?? []; + + // Sort error groups first, and only get timeseries for data in view. + // This is to limit the possibility of creating too many buckets. + + const sortedAndSlicedErrorGroups = orderBy( + errorGroups, + (group) => { + if (sortField === 'occurrences') { + return group.occurrences.value; + } + return group[sortField]; + }, + [sortDirection] + ).slice(pageIndex * size, pageIndex * size + size); + + const sortedErrorGroupIds = sortedAndSlicedErrorGroups.map( + (group) => group.group_id + ); + + const timeseriesResponse = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { terms: { [ERROR_GROUP_ID]: sortedErrorGroupIds } }, + { term: { [SERVICE_NAME]: serviceName } }, + { range: rangeFilter(start, end) }, + ...esFilter, + ], + }, + }, + aggs: { + error_groups: { + terms: { + field: ERROR_GROUP_ID, + size, + }, + aggs: { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { + min: start, + max: end, + }, + }, + }, + }, + }, + }, + }, + }); + + return { + total_error_groups: errorGroups.length, + is_aggregation_accurate: + (response.aggregations?.error_groups.sum_other_doc_count ?? 0) === 0, + error_groups: sortedAndSlicedErrorGroups.map((errorGroup) => ({ + ...errorGroup, + occurrences: { + ...errorGroup.occurrences, + timeseries: + timeseriesResponse.aggregations?.error_groups.buckets + .find((bucket) => bucket.key === errorGroup.group_id) + ?.timeseries.buckets.map((dateBucket) => ({ + x: dateBucket.key, + y: dateBucket.doc_count, + })) ?? null, + }, + })), + }; +} diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 2fbe404a70d829..34551c35ee2342 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -21,6 +21,7 @@ import { serviceNodeMetadataRoute, serviceAnnotationsRoute, serviceAnnotationsCreateRoute, + serviceErrorGroupsRoute, } from './services'; import { agentConfigurationRoute, @@ -115,6 +116,7 @@ const createApmApi = () => { .add(serviceNodeMetadataRoute) .add(serviceAnnotationsRoute) .add(serviceAnnotationsCreateRoute) + .add(serviceErrorGroupsRoute) // Agent configuration .add(getSingleAgentConfigurationRoute) diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 590b6c49d71bf8..ada1674d4555db 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -17,6 +17,8 @@ import { uiFiltersRt, rangeRt } from './default_api_types'; import { getServiceAnnotations } from '../lib/services/annotations'; import { dateAsStringRt } from '../../common/runtime_types/date_as_string_rt'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; +import { getServiceErrorGroups } from '../lib/services/get_service_error_groups'; +import { toNumberRt } from '../../common/runtime_types/to_number_rt'; export const servicesRoute = createRoute(() => ({ path: '/api/apm/services', @@ -195,3 +197,45 @@ export const serviceAnnotationsCreateRoute = createRoute(() => ({ }); }, })); + +export const serviceErrorGroupsRoute = createRoute(() => ({ + path: '/api/apm/services/{serviceName}/error_groups', + params: { + path: t.type({ + serviceName: t.string, + }), + query: t.intersection([ + rangeRt, + uiFiltersRt, + t.type({ + size: toNumberRt, + numBuckets: toNumberRt, + pageIndex: toNumberRt, + sortDirection: t.union([t.literal('asc'), t.literal('desc')]), + sortField: t.union([ + t.literal('last_seen'), + t.literal('occurrences'), + t.literal('name'), + ]), + }), + ]), + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + const { + path: { serviceName }, + query: { size, numBuckets, pageIndex, sortDirection, sortField }, + } = context.params; + + return getServiceErrorGroups({ + serviceName, + setup, + size, + numBuckets, + pageIndex, + sortDirection, + sortField, + }); + }, +})); diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts index d1c5256c81c63d..c2e62b6e1898b7 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts @@ -46,9 +46,13 @@ describe('Workload Statistics Aggregator', () => { aggregations: { taskType: { buckets: [], + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, }, schedule: { buckets: [], + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, }, idleTasks: { doc_count: 0, @@ -158,6 +162,8 @@ describe('Workload Statistics Aggregator', () => { }, aggregations: { schedule: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, buckets: [ { key: '3600s', @@ -174,11 +180,15 @@ describe('Workload Statistics Aggregator', () => { ], }, taskType: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, buckets: [ { key: 'actions_telemetry', doc_count: 2, status: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, buckets: [ { key: 'idle', @@ -191,6 +201,8 @@ describe('Workload Statistics Aggregator', () => { key: 'alerting_telemetry', doc_count: 1, status: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, buckets: [ { key: 'idle', @@ -203,6 +215,8 @@ describe('Workload Statistics Aggregator', () => { key: 'session_cleanup', doc_count: 1, status: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, buckets: [ { key: 'idle', @@ -608,6 +622,7 @@ describe('padBuckets', () => { key: 1601668047000, doc_count: 1, interval: { + doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [], }, @@ -617,6 +632,7 @@ describe('padBuckets', () => { key: 1601668050000, doc_count: 1, interval: { + doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [], }, @@ -626,6 +642,7 @@ describe('padBuckets', () => { key: 1601668053000, doc_count: 0, interval: { + doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [], }, @@ -635,6 +652,7 @@ describe('padBuckets', () => { key: 1601668056000, doc_count: 0, interval: { + doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [], }, @@ -644,6 +662,7 @@ describe('padBuckets', () => { key: 1601668059000, doc_count: 0, interval: { + doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [], }, @@ -653,6 +672,7 @@ describe('padBuckets', () => { key: 1601668062000, doc_count: 1, interval: { + doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [], }, @@ -678,13 +698,13 @@ describe('padBuckets', () => { key_as_string: '2020-10-02T20:40:09.000Z', key: 1601671209000, doc_count: 1, - interval: { buckets: [] }, + interval: { buckets: [], sum_other_doc_count: 0, doc_count_error_upper_bound: 0 }, }, { key_as_string: '2020-10-02T20:40:12.000Z', key: 1601671212000, doc_count: 1, - interval: { buckets: [] }, + interval: { buckets: [], sum_other_doc_count: 0, doc_count_error_upper_bound: 0 }, }, ], }, @@ -707,13 +727,13 @@ describe('padBuckets', () => { key_as_string: '2020-10-02T20:40:09.000Z', key: 1601671209000, doc_count: 1, - interval: { buckets: [] }, + interval: { buckets: [], sum_other_doc_count: 0, doc_count_error_upper_bound: 0 }, }, { key_as_string: '2020-10-02T20:40:12.000Z', key: 1601671212000, doc_count: 1, - interval: { buckets: [] }, + interval: { buckets: [], sum_other_doc_count: 0, doc_count_error_upper_bound: 0 }, }, ], }, @@ -796,7 +816,7 @@ function mockHistogram( key_as_string: key.toISOString(), key: key.getTime(), doc_count: count, - interval: { buckets: [] }, + interval: { buckets: [], doc_count_error_upper_bound: 0, sum_other_doc_count: 0 }, }); } return histogramBuckets; @@ -806,6 +826,8 @@ function mockHistogram( key: number; doc_count: number; interval: { + doc_count_error_upper_bound: number; + sum_other_doc_count: number; buckets: Array<{ key: string; doc_count: number; diff --git a/x-pack/test/apm_api_integration/basic/tests/index.ts b/x-pack/test/apm_api_integration/basic/tests/index.ts index df3e60d79aca57..39dd721c7067e9 100644 --- a/x-pack/test/apm_api_integration/basic/tests/index.ts +++ b/x-pack/test/apm_api_integration/basic/tests/index.ts @@ -25,6 +25,10 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont loadTestFile(require.resolve('./services/transaction_types')); }); + describe('Service overview', function () { + loadTestFile(require.resolve('./service_overview/error_groups')); + }); + describe('Settings', function () { loadTestFile(require.resolve('./settings/custom_link')); loadTestFile(require.resolve('./settings/agent_configuration')); diff --git a/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts b/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts new file mode 100644 index 00000000000000..b699a30d40418c --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts @@ -0,0 +1,220 @@ +/* + * 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 qs from 'querystring'; +import { pick, uniqBy } from 'lodash'; +import { expectSnapshot } from '../../../common/match_snapshot'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import archives from '../../../common/archives_metadata'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const archiveName = 'apm_8.0.0'; + const { start, end } = archives[archiveName]; + + describe('Service overview error groups', () => { + describe('when data is not loaded', () => { + it('handles the empty state', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'occurrences', + })}` + ); + + expect(response.status).to.be(200); + expect(response.body).to.eql({ + total_error_groups: 0, + error_groups: [], + is_aggregation_accurate: true, + }); + }); + }); + + describe('when data is loaded', () => { + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); + + it('returns the correct data', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'occurrences', + })}` + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body.total_error_groups).toMatchInline(`5`); + + expectSnapshot(response.body.error_groups.map((group: any) => group.name)).toMatchInline(` + Array [ + "Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getCost(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getCost() (through reference chain: co.elastic.apm.opbeans.repositories.Stats[\\"numbers\\"]->com.sun.proxy.$Proxy133[\\"cost\\"])", + "java.io.IOException: Connection reset by peer", + "Connection reset by peer", + "Could not write JSON: Unable to find co.elastic.apm.opbeans.model.Customer with id 6617; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Unable to find co.elastic.apm.opbeans.model.Customer with id 6617 (through reference chain: co.elastic.apm.opbeans.model.Customer_$$_jvst369_3[\\"email\\"])", + "Request method 'POST' not supported", + ] + `); + + expectSnapshot(response.body.error_groups.map((group: any) => group.occurrences.value)) + .toMatchInline(` + Array [ + 8, + 2, + 1, + 1, + 1, + ] + `); + + const firstItem = response.body.error_groups[0]; + + expectSnapshot(pick(firstItem, 'group_id', 'last_seen', 'name', 'occurrences.value')) + .toMatchInline(` + Object { + "group_id": "051f95eabf120ebe2f8b0399fe3e54c5", + "last_seen": 1601391561523, + "name": "Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getCost(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getCost() (through reference chain: co.elastic.apm.opbeans.repositories.Stats[\\"numbers\\"]->com.sun.proxy.$Proxy133[\\"cost\\"])", + "occurrences": Object { + "value": 8, + }, + } + `); + + expectSnapshot( + firstItem.occurrences.timeseries.filter(({ y }: any) => y > 0).length + ).toMatchInline(`7`); + }); + + it('sorts items in the correct order', async () => { + const descendingResponse = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'occurrences', + })}` + ); + + expect(descendingResponse.status).to.be(200); + + const descendingOccurrences = descendingResponse.body.error_groups.map( + (item: any) => item.occurrences.value + ); + + expect(descendingOccurrences).to.eql(descendingOccurrences.concat().sort().reverse()); + + const ascendingResponse = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'occurrences', + })}` + ); + + const ascendingOccurrences = ascendingResponse.body.error_groups.map( + (item: any) => item.occurrences.value + ); + + expect(ascendingOccurrences).to.eql(ascendingOccurrences.concat().sort().reverse()); + }); + + it('sorts items by the correct field', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'last_seen', + })}` + ); + + expect(response.status).to.be(200); + + const dates = response.body.error_groups.map((group: any) => group.last_seen); + + expect(dates).to.eql(dates.concat().sort().reverse()); + }); + + it('paginates through the items', async () => { + const size = 1; + + const firstPage = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'occurrences', + })}` + ); + + expect(firstPage.status).to.eql(200); + + const totalItems = firstPage.body.total_error_groups; + + const pages = Math.floor(totalItems / size); + + const items = await new Array(pages) + .fill(undefined) + .reduce(async (prevItemsPromise, _, pageIndex) => { + const prevItems = await prevItemsPromise; + + const thisPage = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size, + numBuckets: 20, + pageIndex, + sortDirection: 'desc', + sortField: 'occurrences', + })}` + ); + + return prevItems.concat(thisPage.body.error_groups); + }, Promise.resolve([])); + + expect(items.length).to.eql(totalItems); + + expect(uniqBy(items, 'group_id').length).to.eql(totalItems); + }); + }); + }); +} diff --git a/x-pack/typings/elasticsearch/aggregations.d.ts b/x-pack/typings/elasticsearch/aggregations.d.ts index 29c78e93831751..bc9ed447c8717f 100644 --- a/x-pack/typings/elasticsearch/aggregations.d.ts +++ b/x-pack/typings/elasticsearch/aggregations.d.ts @@ -204,6 +204,8 @@ type SubAggregationResponseOf< interface AggregationResponsePart { terms: { + doc_count_error_upper_bound: number; + sum_other_doc_count: number; buckets: Array< { doc_count: number; From 58ad7ecd5adae6db8b91f648444ca045f3b5404e Mon Sep 17 00:00:00 2001 From: Bohdan Tsymbala Date: Thu, 12 Nov 2020 15:53:53 +0100 Subject: [PATCH 23/52] Btsymbala/registered av (#81910) * Moved out type for OperatingSystem and moved OS translations one level higher. * Changed the translation to be consistent between trusted apps and policy. * Unified translations of OS types between trusted apps and policy. * Removed unused types. * Added registered AV form section. * Changed the property structure to match the format expected by endpoint. * Fixed the visual alignment of titles in the form and added responsiveness. * Updated snapshots. * Moved out type for OperatingSystem and moved OS translations one level higher. * Added config form heading component. * Cleaned up translations. * Fixed type error with initialization. * Fixed error in trusted app creation form test. * Removed the guard for now in favour of better initialization. * Fixed the store test. * Fixing functional test data. * Added functional test config option to account for a custom header within security app. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/endpoint/models/policy_config.ts | 3 + .../common/endpoint/types/index.ts | 9 +- .../common/endpoint/types/os.ts | 10 +++ .../common/endpoint/types/trusted_apps.ts | 5 +- .../public/management/common/translations.ts | 14 +++ .../policy/store/policy_details/action.ts | 10 ++- .../policy/store/policy_details/index.test.ts | 3 + .../policy/store/policy_details/reducer.ts | 45 +++++++++- .../policy/store/policy_details/selectors.ts | 5 ++ .../public/management/pages/policy/types.ts | 62 ------------- .../components/config_form/index.stories.tsx | 60 +++++++++++++ .../view/components/config_form/index.tsx | 84 +++++++++++++++++ .../pages/policy/view/policy_details.tsx | 3 + .../antivirus_registration/index.tsx | 64 +++++++++++++ .../policy/view/policy_forms/config_form.tsx | 89 ------------------- .../policy/view/policy_forms/events/linux.tsx | 45 ++++------ .../policy/view/policy_forms/events/mac.tsx | 45 ++++------ .../view/policy_forms/events/translations.ts | 28 ++++++ .../view/policy_forms/events/windows.tsx | 45 ++++------ .../view/policy_forms/protections/malware.tsx | 39 ++++---- .../create_trusted_app_form.test.tsx | 2 +- .../__snapshots__/index.test.tsx.snap | 4 +- .../__snapshots__/index.test.tsx.snap | 18 ++-- .../__snapshots__/index.test.tsx.snap | 56 ++++++------ .../pages/trusted_apps/view/translations.ts | 14 +-- .../translations/translations/ja-JP.json | 7 -- .../translations/translations/zh-CN.json | 7 -- .../apps/endpoint/policy_details.ts | 3 + .../test/security_solution_endpoint/config.ts | 3 + 29 files changed, 447 insertions(+), 335 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/endpoint/types/os.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/config_form.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/translations.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts index 3250e048edad21..890def5b63d4a7 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts @@ -33,6 +33,9 @@ export const factory = (): PolicyConfig => { logging: { file: 'info', }, + antivirus_registration: { + enabled: false, + }, }, mac: { events: { diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index 1d64578a6a7f11..673d04c856935f 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -8,6 +8,7 @@ import { ApplicationStart } from 'kibana/public'; import { NewPackagePolicy, PackagePolicy } from '../../../../fleet/common'; import { ManifestSchema } from '../schema/manifest'; +export * from './os'; export * from './trusted_apps'; /** @@ -880,6 +881,9 @@ export interface PolicyConfig { enabled: boolean; }; }; + antivirus_registration: { + enabled: boolean; + }; }; mac: { advanced?: {}; @@ -919,7 +923,10 @@ export interface UIPolicyConfig { /** * Windows-specific policy configuration that is supported via the UI */ - windows: Pick; + windows: Pick< + PolicyConfig['windows'], + 'events' | 'malware' | 'popup' | 'antivirus_registration' | 'advanced' + >; /** * Mac-specific policy configuration that is supported via the UI */ diff --git a/x-pack/plugins/security_solution/common/endpoint/types/os.ts b/x-pack/plugins/security_solution/common/endpoint/types/os.ts new file mode 100644 index 00000000000000..b9afbd63ecd54a --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/types/os.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type Linux = 'linux'; +export type MacOS = 'macos'; +export type Windows = 'windows'; +export type OperatingSystem = Linux | MacOS | Windows; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts index 3568136dd0e7b9..79d66443bc8f13 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts @@ -11,6 +11,7 @@ import { GetTrustedAppsRequestSchema, PostTrustedAppCreateRequestSchema, } from '../schema/trusted_apps'; +import { Linux, MacOS, Windows } from './os'; /** API request params for deleting Trusted App entry */ export type DeleteTrustedAppsRequestParams = TypeOf; @@ -51,11 +52,11 @@ export type NewTrustedApp = { description?: string; } & ( | { - os: 'linux' | 'macos'; + os: Linux | MacOS; entries: MacosLinuxConditionEntry[]; } | { - os: 'windows'; + os: Windows; entries: WindowsConditionEntry[]; } ); diff --git a/x-pack/plugins/security_solution/public/management/common/translations.ts b/x-pack/plugins/security_solution/public/management/common/translations.ts index d24eb1bd315fa7..415658c1fd6afd 100644 --- a/x-pack/plugins/security_solution/public/management/common/translations.ts +++ b/x-pack/plugins/security_solution/public/management/common/translations.ts @@ -6,6 +6,8 @@ import { i18n } from '@kbn/i18n'; +import { OperatingSystem } from '../../../common/endpoint/types'; + export const ENDPOINTS_TAB = i18n.translate('xpack.securitySolution.endpointsTab', { defaultMessage: 'Endpoints', }); @@ -21,3 +23,15 @@ export const TRUSTED_APPS_TAB = i18n.translate('xpack.securitySolution.trustedAp export const BETA_BADGE_LABEL = i18n.translate('xpack.securitySolution.administration.list.beta', { defaultMessage: 'Beta', }); + +export const OS_TITLES: Readonly<{ [K in OperatingSystem]: string }> = { + windows: i18n.translate('xpack.securitySolution.administration.os.windows', { + defaultMessage: 'Windows', + }), + macos: i18n.translate('xpack.securitySolution.administration.os.macos', { + defaultMessage: 'Mac', + }), + linux: i18n.translate('xpack.securitySolution.administration.os.linux', { + defaultMessage: 'Linux', + }), +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action.ts index f5a219bce4a6bd..bda408cd00e75e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action.ts @@ -31,6 +31,13 @@ interface UserChangedPolicyConfig { }; } +interface UserChangedAntivirusRegistration { + type: 'userChangedAntivirusRegistration'; + payload: { + enabled: boolean; + }; +} + interface ServerReturnedPolicyDetailsAgentSummaryData { type: 'serverReturnedPolicyDetailsAgentSummaryData'; payload: { @@ -62,4 +69,5 @@ export type PolicyDetailsAction = | ServerReturnedPolicyDetailsUpdateFailure | ServerReturnedUpdatedPolicyDetailsData | ServerFailedToReturnPolicyDetailsData - | UserChangedPolicyConfig; + | UserChangedPolicyConfig + | UserChangedAntivirusRegistration; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts index 89ba05547f4479..69c2afbd019607 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts @@ -245,6 +245,9 @@ describe('policy details: ', () => { }, }, logging: { file: 'info' }, + antivirus_registration: { + enabled: false, + }, }, mac: { events: { process: true, file: true, network: true }, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts index 43a6ad2c585b43..bcdc7ba2089c64 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts @@ -4,11 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ import { fullPolicy, isOnPolicyDetailsPage } from './selectors'; -import { Immutable, PolicyConfig, UIPolicyConfig } from '../../../../../../common/endpoint/types'; +import { + Immutable, + PolicyConfig, + UIPolicyConfig, + PolicyData, +} from '../../../../../../common/endpoint/types'; import { ImmutableReducer } from '../../../../../common/store'; import { AppAction } from '../../../../../common/store/actions'; import { PolicyDetailsState } from '../../types'; +const updatePolicyConfigInPolicyData = ( + policyData: Immutable, + policyConfig: Immutable +) => ({ + ...policyData, + inputs: policyData.inputs.map((input) => ({ + ...input, + config: input.config && { + ...input.config, + policy: { + ...input.config.policy, + value: policyConfig, + }, + }, + })), +}); + /** * Return a fresh copy of initial state, since we mutate state in the reducer. */ @@ -126,5 +148,26 @@ export const policyDetailsReducer: ImmutableReducer UIPolicyConfig = createSel events: windows.events, malware: windows.malware, popup: windows.popup, + antivirus_registration: windows.antivirus_registration, }, mac: { advanced: mac.advanced, @@ -122,6 +123,10 @@ export const policyConfig: (s: PolicyDetailsState) => UIPolicyConfig = createSel } ); +export const isAntivirusRegistrationEnabled = createSelector(policyConfig, (uiPolicyConfig) => { + return uiPolicyConfig.windows.antivirus_registration.enabled; +}); + /** Returns the total number of possible windows eventing configurations */ export const totalWindowsEvents = (state: PolicyDetailsState): number => { const config = policyConfig(state); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts index 152caff3714b0a..3926ad2220e35d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts @@ -76,68 +76,6 @@ export interface PolicyListUrlSearchParams { page_size: number; } -/** - * Endpoint Policy configuration - */ -export interface PolicyConfig { - windows: { - events: { - dll_and_driver_load: boolean; - dns: boolean; - file: boolean; - network: boolean; - process: boolean; - registry: boolean; - security: boolean; - }; - malware: MalwareFields; - logging: { - stdout: string; - file: string; - }; - advanced: PolicyConfigAdvancedOptions; - }; - mac: { - events: { - file: boolean; - process: boolean; - network: boolean; - }; - malware: MalwareFields; - logging: { - stdout: string; - file: string; - }; - advanced: PolicyConfigAdvancedOptions; - }; - linux: { - events: { - file: boolean; - process: boolean; - network: boolean; - }; - logging: { - stdout: string; - file: string; - }; - advanced: PolicyConfigAdvancedOptions; - }; -} - -interface PolicyConfigAdvancedOptions { - elasticsearch: { - indices: { - control: string; - event: string; - logging: string; - }; - kernel: { - connect: boolean; - process: boolean; - }; - }; -} - export enum OS { windows = 'windows', mac = 'mac', diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx new file mode 100644 index 00000000000000..4f288af393b7c0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx @@ -0,0 +1,60 @@ +/* + * 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 { ThemeProvider } from 'styled-components'; +import { storiesOf, addDecorator } from '@storybook/react'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { EuiCheckbox, EuiSpacer, EuiSwitch, EuiText } from '@elastic/eui'; + +import { ConfigForm } from '.'; + +addDecorator((storyFn) => ( + ({ eui: euiLightVars, darkMode: false })}>{storyFn()} +)); + +storiesOf('PolicyDetails/ConfigForm', module) + .add('One OS', () => { + return ( + + {'Some content'} + + ); + }) + .add('Multiple OSs', () => { + return ( + + {'Some content'} + + ); + }) + .add('Complex content', () => { + return ( + + + {'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore ' + + 'et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut ' + + 'aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum ' + + 'dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia ' + + 'deserunt mollit anim id est laborum.'} + + + {}} /> + + {}} /> + {}} /> + {}} /> + + ); + }) + .add('Right corner content', () => { + const toggle = {}} />; + + return ( + + {'Some content'} + + ); + }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx new file mode 100644 index 00000000000000..30c35de9b907f3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, ReactNode, memo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiHorizontalRule, + EuiText, + EuiShowFor, + EuiPanel, +} from '@elastic/eui'; + +import { OperatingSystem } from '../../../../../../../common/endpoint/types'; +import { OS_TITLES } from '../../../../../common/translations'; + +const TITLES = { + type: i18n.translate('xpack.securitySolution.endpoint.policyDetailType', { + defaultMessage: 'Type', + }), + os: i18n.translate('xpack.securitySolution.endpoint.policyDetailOS', { + defaultMessage: 'Operating System', + }), +}; + +interface ConfigFormProps { + /** + * A subtitle for this component. + **/ + type: string; + /** + * Types of supported operating systems. + */ + supportedOss: OperatingSystem[]; + dataTestSubj?: string; + /** React Node to be put on the right corner of the card */ + rightCorner?: ReactNode; +} + +export const ConfigFormHeading: FC = memo(({ children }) => ( + +
{children}
+
+)); + +ConfigFormHeading.displayName = 'ConfigFormHeading'; + +export const ConfigForm: FC = memo( + ({ type, supportedOss, dataTestSubj, rightCorner, children }) => ( + + + + {TITLES.type} + {type} + + + {TITLES.os} + {supportedOss.map((os) => OS_TITLES[os]).join(', ')} + + + + + {rightCorner} + + + + + {rightCorner} + + + + + + {children} + + ) +); + +ConfigForm.displayName = 'ConfigForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx index 8fc5de48f36db2..9c11bc6f5a4d13 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx @@ -36,6 +36,7 @@ import { AgentsSummary } from './agents_summary'; import { VerticalDivider } from './vertical_divider'; import { WindowsEvents, MacEvents, LinuxEvents } from './policy_forms/events'; import { MalwareProtections } from './policy_forms/protections/malware'; +import { AntivirusRegistrationForm } from './policy_forms/antivirus_registration'; import { useToasts } from '../../../../common/lib/kibana'; import { AppAction } from '../../../../common/store/actions'; import { SpyRoute } from '../../../../common/utils/route/spy_routes'; @@ -251,6 +252,8 @@ export const PolicyDetails = React.memo(() => { + + diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx new file mode 100644 index 00000000000000..8d1ac29c8ce1e7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { i18n } from '@kbn/i18n'; +import { EuiSpacer, EuiSwitch, EuiText } from '@elastic/eui'; + +import { isAntivirusRegistrationEnabled } from '../../../store/policy_details/selectors'; +import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { ConfigForm } from '../../components/config_form'; + +export const AntivirusRegistrationForm = memo(() => { + const antivirusRegistrationEnabled = usePolicyDetailsSelector(isAntivirusRegistrationEnabled); + const dispatch = useDispatch(); + + const handleSwitchChange = useCallback( + (event) => + dispatch({ + type: 'userChangedAntivirusRegistration', + payload: { + enabled: event.target.checked, + }, + }), + [dispatch] + ); + + return ( + + + {i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.explanation', + { + defaultMessage: 'Switch the toggle to on to register Elastic anti-virus', + } + )} + + + + + ); +}); + +AntivirusRegistrationForm.displayName = 'AntivirusRegistrationForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/config_form.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/config_form.tsx deleted file mode 100644 index 8e3c4138efb36e..00000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/config_form.tsx +++ /dev/null @@ -1,89 +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, { useMemo } from 'react'; -import { - EuiCard, - EuiFlexGroup, - EuiFlexItem, - EuiTitle, - EuiHorizontalRule, - EuiText, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import styled from 'styled-components'; - -const PolicyDetailCard = styled.div` - .policyDetailTitleOS { - flex-grow: 2; - } - .policyDetailTitleFlexItem { - margin: 0; - } -`; -export const ConfigForm: React.FC<{ - /** - * A subtitle for this component. - **/ - type: string; - /** - * Types of supported operating systems. - */ - supportedOss: React.ReactNode; - children: React.ReactNode; - dataTestSubj: string; - /** React Node to be put on the right corner of the card */ - rightCorner: React.ReactNode; -}> = React.memo(({ type, supportedOss, children, dataTestSubj, rightCorner }) => { - const typeTitle = useMemo(() => { - return ( - - - - -
- -
-
-
- - {type} - -
- - - -
- -
-
-
- - {supportedOss} - -
- {rightCorner} -
- ); - }, [rightCorner, supportedOss, type]); - - return ( - - - - {children} - - - ); -}); - -ConfigForm.displayName = 'ConfigForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx index 66126adb7a4e1d..b43f93f1a1e2be 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx @@ -6,15 +6,19 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; +import { EuiText, EuiSpacer } from '@elastic/eui'; import { EventsCheckbox } from './checkbox'; import { OS } from '../../../types'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedLinuxEvents, totalLinuxEvents } from '../../../store/policy_details/selectors'; -import { ConfigForm } from '../config_form'; +import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; import { getIn, setIn } from '../../../models/policy_details_config'; import { UIPolicyConfig } from '../../../../../../../common/endpoint/types'; +import { + COLLECTIONS_ENABLED_MESSAGE, + EVENTS_FORM_TYPE_LABEL, + EVENTS_HEADING, +} from './translations'; export const LinuxEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedLinuxEvents); @@ -59,14 +63,7 @@ export const LinuxEvents = React.memo(() => { ]; return ( <> - -
- -
-
+ {EVENTS_HEADING} {items.map((item, index) => { return ( @@ -85,28 +82,16 @@ export const LinuxEvents = React.memo(() => { ); }, []); - const collectionsEnabled = useMemo(() => { - return ( - - - - ); - }, [selected, total]); - return ( + {COLLECTIONS_ENABLED_MESSAGE(selected, total)} + + } > {checkboxes} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx index dc70fc0ba0f4fd..fbbe50fbec1b03 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx @@ -6,15 +6,19 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; +import { EuiText, EuiSpacer } from '@elastic/eui'; import { EventsCheckbox } from './checkbox'; import { OS } from '../../../types'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedMacEvents, totalMacEvents } from '../../../store/policy_details/selectors'; -import { ConfigForm } from '../config_form'; +import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; import { getIn, setIn } from '../../../models/policy_details_config'; import { UIPolicyConfig } from '../../../../../../../common/endpoint/types'; +import { + COLLECTIONS_ENABLED_MESSAGE, + EVENTS_FORM_TYPE_LABEL, + EVENTS_HEADING, +} from './translations'; export const MacEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedMacEvents); @@ -59,14 +63,7 @@ export const MacEvents = React.memo(() => { ]; return ( <> - -
- -
-
+ {EVENTS_HEADING} {items.map((item, index) => { return ( @@ -85,28 +82,16 @@ export const MacEvents = React.memo(() => { ); }, []); - const collectionsEnabled = useMemo(() => { - return ( - - - - ); - }, [selected, total]); - return ( + {COLLECTIONS_ENABLED_MESSAGE(selected, total)} + + } > {checkboxes} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/translations.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/translations.ts new file mode 100644 index 00000000000000..3b48b7969a8ced --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/translations.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const EVENTS_HEADING = i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.eventingEvents', + { + defaultMessage: 'Events', + } +); + +export const EVENTS_FORM_TYPE_LABEL = i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.eventCollection', + { + defaultMessage: 'Event Collection', + } +); + +export const COLLECTIONS_ENABLED_MESSAGE = (selected: number, total: number) => { + return i18n.translate('xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled', { + defaultMessage: '{selected} / {total} event collections enabled', + values: { selected, total }, + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx index 5acdf67922a3a6..f7b1a8e901ed2c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx @@ -6,15 +6,19 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; +import { EuiText, EuiSpacer } from '@elastic/eui'; import { EventsCheckbox } from './checkbox'; import { OS } from '../../../types'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedWindowsEvents, totalWindowsEvents } from '../../../store/policy_details/selectors'; -import { ConfigForm } from '../config_form'; +import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; import { setIn, getIn } from '../../../models/policy_details_config'; import { UIPolicyConfig, Immutable } from '../../../../../../../common/endpoint/types'; +import { + COLLECTIONS_ENABLED_MESSAGE, + EVENTS_FORM_TYPE_LABEL, + EVENTS_HEADING, +} from './translations'; export const WindowsEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedWindowsEvents); @@ -99,14 +103,7 @@ export const WindowsEvents = React.memo(() => { ]; return ( <> - -
- -
-
+ {EVENTS_HEADING} {items.map((item, index) => { return ( @@ -125,28 +122,16 @@ export const WindowsEvents = React.memo(() => { ); }, []); - const collectionsEnabled = useMemo(() => { - return ( - - - - ); - }, [selected, total]); - return ( + {COLLECTIONS_ENABLED_MESSAGE(selected, total)} + + } > {checkboxes} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx index b61dee52697370..7259b2ec19ee2a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -7,10 +7,11 @@ import React, { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiRadio, EuiSwitch, - EuiTitle, EuiText, EuiSpacer, EuiTextArea, @@ -18,15 +19,13 @@ import { EuiCallOut, EuiCheckbox, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import { cloneDeep } from 'lodash'; import { APP_ID } from '../../../../../../../common/constants'; import { SecurityPageName } from '../../../../../../app/types'; import { Immutable, ProtectionModes } from '../../../../../../../common/endpoint/types'; import { OS, MalwareProtectionOSes } from '../../../types'; -import { ConfigForm } from '../config_form'; +import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; import { policyConfig } from '../../../store/policy_details/selectors'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { LinkToApp } from '../../../../../../common/components/endpoint/link_to_app'; @@ -200,14 +199,12 @@ export const MalwareProtections = React.memo(() => { const radioButtons = useMemo(() => { return ( <> - -
- -
-
+ + + {radios.map((radio) => { @@ -221,14 +218,12 @@ export const MalwareProtections = React.memo(() => { })} - -
- -
-
+ + + { type={i18n.translate('xpack.securitySolution.endpoint.policy.details.malware', { defaultMessage: 'Malware', })} - supportedOss={i18n.translate('xpack.securitySolution.endpoint.policy.details.windowsAndMac', { - defaultMessage: 'Windows, Mac', - })} + supportedOss={['windows', 'macos']} dataTestSubj="malwareProtectionsForm" rightCorner={protectionSwitch} > diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx index 211fc9ec3371e1..4bac9164e1d621 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx @@ -118,7 +118,7 @@ describe('When showing the Trusted App Create Form', () => { '.euiSuperSelect__listbox button.euiSuperSelect__item' ) ).map((button) => button.textContent); - expect(options).toEqual(['Mac OS', 'Windows', 'Linux']); + expect(options).toEqual(['Mac', 'Windows', 'Linux']); }); it('should show Description as optional', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap index a94e6287a4f58b..a47558257420c5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap @@ -17,7 +17,7 @@ exports[`trusted_app_card TrustedAppCard should render correctly 1`] = ` value={ } /> @@ -112,7 +112,7 @@ exports[`trusted_app_card TrustedAppCard should trim long texts 1`] = ` value={ } /> diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap index c82b9cac8ab1fc..6d45059099f8d3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap @@ -412,7 +412,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiToolTipAnchor" > - Mac OS + Mac @@ -1168,7 +1168,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiToolTipAnchor" > - Mac OS + Mac @@ -1924,7 +1924,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiToolTipAnchor" > - Mac OS + Mac @@ -3222,7 +3222,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiToolTipAnchor" > - Mac OS + Mac @@ -3978,7 +3978,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiToolTipAnchor" > - Mac OS + Mac @@ -4734,7 +4734,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiToolTipAnchor" > - Mac OS + Mac @@ -5990,7 +5990,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiToolTipAnchor" > - Mac OS + Mac @@ -6746,7 +6746,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiToolTipAnchor" > - Mac OS + Mac @@ -7502,7 +7502,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiToolTipAnchor" > - Mac OS + Mac diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap index 2797c433b8236c..d0459871d48811 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap @@ -1061,7 +1061,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -1448,7 +1448,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -1835,7 +1835,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -2222,7 +2222,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -2609,7 +2609,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -2996,7 +2996,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -3383,7 +3383,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -4001,7 +4001,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -4388,7 +4388,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -4775,7 +4775,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -5162,7 +5162,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -5549,7 +5549,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -5936,7 +5936,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -6323,7 +6323,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -7099,7 +7099,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -7486,7 +7486,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -7873,7 +7873,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -8260,7 +8260,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -8647,7 +8647,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -9034,7 +9034,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -9421,7 +9421,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -10039,7 +10039,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -10426,7 +10426,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -10813,7 +10813,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -11200,7 +11200,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -11587,7 +11587,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -11974,7 +11974,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -12361,7 +12361,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts index b2f62c2f1da4eb..4c2b3f0e59ccb4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts @@ -11,23 +11,13 @@ import { WindowsConditionEntry, } from '../../../../../common/endpoint/types'; +export { OS_TITLES } from '../../../common/translations'; + export const ABOUT_TRUSTED_APPS = i18n.translate('xpack.securitySolution.trustedapps.aboutInfo', { defaultMessage: 'Add a trusted application to improve performance or alleviate conflicts with other applications running on your hosts. Trusted applications will be applied to hosts running Endpoint Security.', }); -export const OS_TITLES: Readonly<{ [K in TrustedApp['os']]: string }> = { - windows: i18n.translate('xpack.securitySolution.trustedapps.os.windows', { - defaultMessage: 'Windows', - }), - macos: i18n.translate('xpack.securitySolution.trustedapps.os.macos', { - defaultMessage: 'Mac OS', - }), - linux: i18n.translate('xpack.securitySolution.trustedapps.os.linux', { - defaultMessage: 'Linux', - }), -}; - type Entry = MacosLinuxConditionEntry | WindowsConditionEntry; export const CONDITION_FIELD_TITLE: { [K in Entry['field']]: string } = { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 852c3ffd9bfd67..912cb01d458e2e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -17498,8 +17498,6 @@ "xpack.securitySolution.endpoint.policy.details.detectionRulesMessage": "{detectionRulesLink}を表示します。事前構築済みルールは、[検出ルール]ページで「Elastic」というタグが付けられています。", "xpack.securitySolution.endpoint.policy.details.eventCollection": "イベント収集", "xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled": "{selected} / {total}件のイベント収集が有効です", - "xpack.securitySolution.endpoint.policy.details.linux": "Linux", - "xpack.securitySolution.endpoint.policy.details.mac": "Mac", "xpack.securitySolution.endpoint.policy.details.malware": "マルウェア", "xpack.securitySolution.endpoint.policy.details.malwareProtectionsEnabled": "マルウェア保護{mode, select, true {有効} false {無効}}", "xpack.securitySolution.endpoint.policy.details.prevent": "防御", @@ -17514,8 +17512,6 @@ "xpack.securitySolution.endpoint.policy.details.updateErrorTitle": "失敗しました。", "xpack.securitySolution.endpoint.policy.details.updateSuccessMessage": "統合{name}が更新されました。", "xpack.securitySolution.endpoint.policy.details.updateSuccessTitle": "成功!", - "xpack.securitySolution.endpoint.policy.details.windows": "Windows", - "xpack.securitySolution.endpoint.policy.details.windowsAndMac": "Windows、Mac", "xpack.securitySolution.endpoint.policyDetailOS": "オペレーティングシステム", "xpack.securitySolution.endpoint.policyDetails.agentsSummary.errorTitle": "エラー", "xpack.securitySolution.endpoint.policyDetails.agentsSummary.offlineTitle": "オフライン", @@ -18466,9 +18462,6 @@ "xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND", "xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "条件が定義されていません", "xpack.securitySolution.trustedapps.noResults": "項目が見つかりません", - "xpack.securitySolution.trustedapps.os.linux": "Linux", - "xpack.securitySolution.trustedapps.os.macos": "Mac OS", - "xpack.securitySolution.trustedapps.os.windows": "Windows", "xpack.securitySolution.trustedapps.trustedapp.createdAt": "作成日", "xpack.securitySolution.trustedapps.trustedapp.createdBy": "作成者", "xpack.securitySolution.trustedapps.trustedapp.description": "説明", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 461b93e2e081d7..8ae964d9ee7d09 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -17516,8 +17516,6 @@ "xpack.securitySolution.endpoint.policy.details.detectionRulesMessage": "请查看{detectionRulesLink}。在“检测规则”页面上,预置规则标记有“Elastic”。", "xpack.securitySolution.endpoint.policy.details.eventCollection": "事件收集", "xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled": "{selected} / {total} 事件收集已启用", - "xpack.securitySolution.endpoint.policy.details.linux": "Linux", - "xpack.securitySolution.endpoint.policy.details.mac": "Mac", "xpack.securitySolution.endpoint.policy.details.malware": "恶意软件", "xpack.securitySolution.endpoint.policy.details.malwareProtectionsEnabled": "恶意软件防护{mode, select, true {已启用} false {已禁用}}", "xpack.securitySolution.endpoint.policy.details.prevent": "防御", @@ -17533,8 +17531,6 @@ "xpack.securitySolution.endpoint.policy.details.updateErrorTitle": "失败!", "xpack.securitySolution.endpoint.policy.details.updateSuccessMessage": "集成 {name} 已更新。", "xpack.securitySolution.endpoint.policy.details.updateSuccessTitle": "成功!", - "xpack.securitySolution.endpoint.policy.details.windows": "Windows", - "xpack.securitySolution.endpoint.policy.details.windowsAndMac": "Windows、Mac", "xpack.securitySolution.endpoint.policyDetailOS": "操作系统", "xpack.securitySolution.endpoint.policyDetails.agentsSummary.errorTitle": "错误", "xpack.securitySolution.endpoint.policyDetails.agentsSummary.offlineTitle": "脱机", @@ -18485,9 +18481,6 @@ "xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND", "xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "未定义条件", "xpack.securitySolution.trustedapps.noResults": "找不到项目", - "xpack.securitySolution.trustedapps.os.linux": "Linux", - "xpack.securitySolution.trustedapps.os.macos": "Mac OS", - "xpack.securitySolution.trustedapps.os.windows": "Windows", "xpack.securitySolution.trustedapps.trustedapp.createdAt": "创建日期", "xpack.securitySolution.trustedapps.trustedapp.createdBy": "创建者", "xpack.securitySolution.trustedapps.trustedapp.description": "描述", diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index 15c26a1b9374d6..f032416d2e7bb7 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -221,6 +221,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { message: 'Elastic Security { action } { filename }', }, }, + antivirus_registration: { + enabled: false, + }, }, }, streams: [], diff --git a/x-pack/test/security_solution_endpoint/config.ts b/x-pack/test/security_solution_endpoint/config.ts index 9499c235a5f0d5..f3cb4a5812a5cb 100644 --- a/x-pack/test/security_solution_endpoint/config.ts +++ b/x-pack/test/security_solution_endpoint/config.ts @@ -43,5 +43,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...getRegistryUrlAsArray(), ], }, + layout: { + fixedHeaderHeight: 200, + }, }; } From eaa65535edf5ad7bb64d50373bc7587ca18d1d7f Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 12 Nov 2020 15:54:55 +0100 Subject: [PATCH 24/52] Use saved object references for dashboard drilldowns (#82602) --- ...ver.embeddablesetup.getattributeservice.md | 11 - ...ugins-embeddable-server.embeddablesetup.md | 3 +- .../embeddable/embeddable_references.test.ts | 87 ++++++ .../embeddable/embeddable_references.ts | 82 +++++ ...embeddable_saved_object_converters.test.ts | 26 +- .../embeddable_saved_object_converters.ts | 7 +- .../saved_dashboard_references.test.ts | 164 +++++----- .../saved_dashboard_references.ts | 75 ++++- src/plugins/dashboard/common/types.ts | 17 ++ .../application/dashboard_app_controller.tsx | 2 +- .../application/dashboard_state_manager.ts | 2 +- .../public/application/embeddable/types.ts | 12 +- src/plugins/dashboard/public/plugin.tsx | 1 + .../public/saved_dashboards/index.ts | 2 +- .../saved_dashboards/saved_dashboard.ts | 22 +- .../saved_dashboards/saved_dashboards.ts | 10 +- src/plugins/dashboard/public/types.ts | 10 +- src/plugins/dashboard/server/plugin.ts | 20 +- .../server/saved_objects/dashboard.ts | 15 +- .../dashboard_migrations.test.ts | 55 +++- .../saved_objects/dashboard_migrations.ts | 62 +++- .../dashboard/server/saved_objects/index.ts | 2 +- .../saved_objects/migrations_730.test.ts | 6 +- src/plugins/embeddable/common/index.ts | 21 ++ src/plugins/embeddable/common/lib/index.ts | 1 + .../lib}/saved_object_embeddable.ts | 2 +- src/plugins/embeddable/common/mocks.ts | 31 ++ src/plugins/embeddable/common/types.ts | 15 +- .../public/lib/containers/container.ts | 2 +- .../public/lib/containers/i_container.ts | 12 +- .../public/lib/embeddables/index.ts | 2 +- src/plugins/embeddable/server/mocks.ts | 30 ++ src/plugins/embeddable/server/plugin.ts | 17 +- src/plugins/embeddable/server/server.api.md | 5 +- .../dashboard_drilldown/constants.ts | 14 + ...hboard_drilldown_persistable_state.test.ts | 48 +++ .../dashboard_drilldown_persistable_state.ts | 75 +++++ .../drilldowns/dashboard_drilldown/index.ts | 9 + .../drilldowns/dashboard_drilldown/types.ts | 12 + .../common/drilldowns/index.ts | 7 + .../dashboard_enhanced/common/index.ts | 7 + x-pack/plugins/dashboard_enhanced/kibana.json | 2 +- .../abstract_dashboard_drilldown.tsx | 8 +- .../abstract_dashboard_drilldown/types.ts | 8 +- ...embeddable_to_dashboard_drilldown.test.tsx | 6 + .../embeddable_to_dashboard_drilldown.tsx | 5 + .../dashboard_enhanced/server/index.ts | 19 ++ .../dashboard_enhanced/server/plugin.ts | 44 +++ .../ui_actions_enhanced/common/index.ts | 7 + .../server/dynamic_action_enhancement.ts | 4 +- .../ui_actions_enhanced/server/index.ts | 6 +- .../ui_actions_enhanced/server/plugin.ts | 4 +- .../dashboard_to_dashboard_drilldown.ts | 279 +++++++++++------- .../reporting/hugedata/data.json.gz | Bin 33744 -> 33744 bytes .../spaces/copy_saved_objects/data.json | 4 +- .../kibana/dashboard/sample_dashboard.json | 8 +- .../kibana/dashboard/sample_dashboard2.json | 8 +- .../kibana/dashboard/sample_dashboard.json | 8 +- 58 files changed, 1122 insertions(+), 301 deletions(-) delete mode 100644 docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md create mode 100644 src/plugins/dashboard/common/embeddable/embeddable_references.test.ts create mode 100644 src/plugins/dashboard/common/embeddable/embeddable_references.ts rename src/plugins/dashboard/{public/application/lib => common/embeddable}/embeddable_saved_object_converters.test.ts (82%) rename src/plugins/dashboard/{public/application/lib => common/embeddable}/embeddable_saved_object_converters.ts (91%) rename src/plugins/dashboard/{public/saved_dashboards => common}/saved_dashboard_references.test.ts (52%) rename src/plugins/dashboard/{public/saved_dashboards => common}/saved_dashboard_references.ts (55%) create mode 100644 src/plugins/embeddable/common/index.ts rename src/plugins/embeddable/{public/lib/embeddables => common/lib}/saved_object_embeddable.ts (96%) create mode 100644 src/plugins/embeddable/common/mocks.ts create mode 100644 src/plugins/embeddable/server/mocks.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/constants.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.test.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/index.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/types.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/drilldowns/index.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/index.ts create mode 100644 x-pack/plugins/dashboard_enhanced/server/index.ts create mode 100644 x-pack/plugins/dashboard_enhanced/server/plugin.ts create mode 100644 x-pack/plugins/ui_actions_enhanced/common/index.ts diff --git a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md deleted file mode 100644 index 9cd77ca6e3a368..00000000000000 --- a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-embeddable-server](./kibana-plugin-plugins-embeddable-server.md) > [EmbeddableSetup](./kibana-plugin-plugins-embeddable-server.embeddablesetup.md) > [getAttributeService](./kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md) - -## EmbeddableSetup.getAttributeService property - -Signature: - -```typescript -getAttributeService: any; -``` diff --git a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md index bd024095e80bed..5109a75ad57f06 100644 --- a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md +++ b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md @@ -7,14 +7,13 @@ Signature: ```typescript -export interface EmbeddableSetup +export interface EmbeddableSetup extends PersistableStateService ``` ## Properties | Property | Type | Description | | --- | --- | --- | -| [getAttributeService](./kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md) | any | | | [registerEmbeddableFactory](./kibana-plugin-plugins-embeddable-server.embeddablesetup.registerembeddablefactory.md) | (factory: EmbeddableRegistryDefinition) => void | | | [registerEnhancement](./kibana-plugin-plugins-embeddable-server.embeddablesetup.registerenhancement.md) | (enhancement: EnhancementRegistryDefinition) => void | | diff --git a/src/plugins/dashboard/common/embeddable/embeddable_references.test.ts b/src/plugins/dashboard/common/embeddable/embeddable_references.test.ts new file mode 100644 index 00000000000000..fabc89f8c8233a --- /dev/null +++ b/src/plugins/dashboard/common/embeddable/embeddable_references.test.ts @@ -0,0 +1,87 @@ +/* + * 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 { + ExtractDeps, + extractPanelsReferences, + InjectDeps, + injectPanelsReferences, +} from './embeddable_references'; +import { createEmbeddablePersistableStateServiceMock } from '../../../embeddable/common/mocks'; +import { SavedDashboardPanel } from '../types'; +import { EmbeddableStateWithType } from '../../../embeddable/common'; + +const embeddablePersistableStateService = createEmbeddablePersistableStateServiceMock(); +const deps: InjectDeps & ExtractDeps = { + embeddablePersistableStateService, +}; + +test('inject/extract panel references', () => { + embeddablePersistableStateService.extract.mockImplementationOnce((state) => { + const { HARDCODED_ID, ...restOfState } = (state as unknown) as Record; + return { + state: restOfState as EmbeddableStateWithType, + references: [{ id: HARDCODED_ID as string, name: 'refName', type: 'type' }], + }; + }); + + embeddablePersistableStateService.inject.mockImplementationOnce((state, references) => { + const ref = references.find((r) => r.name === 'refName'); + return { + ...state, + HARDCODED_ID: ref!.id, + }; + }); + + const savedDashboardPanel: SavedDashboardPanel = { + type: 'search', + embeddableConfig: { + HARDCODED_ID: 'IMPORTANT_HARDCODED_ID', + }, + id: 'savedObjectId', + panelIndex: '123', + gridData: { + x: 0, + y: 0, + h: 15, + w: 15, + i: '123', + }, + version: '7.0.0', + }; + + const [{ panel: extractedPanel, references }] = extractPanelsReferences( + [savedDashboardPanel], + deps + ); + expect(extractedPanel.embeddableConfig).toEqual({}); + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "IMPORTANT_HARDCODED_ID", + "name": "refName", + "type": "type", + }, + ] + `); + + const [injectedPanel] = injectPanelsReferences([extractedPanel], references, deps); + + expect(injectedPanel).toEqual(savedDashboardPanel); +}); diff --git a/src/plugins/dashboard/common/embeddable/embeddable_references.ts b/src/plugins/dashboard/common/embeddable/embeddable_references.ts new file mode 100644 index 00000000000000..dd686203fa3516 --- /dev/null +++ b/src/plugins/dashboard/common/embeddable/embeddable_references.ts @@ -0,0 +1,82 @@ +/* + * 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 { omit } from 'lodash'; +import { + convertSavedDashboardPanelToPanelState, + convertPanelStateToSavedDashboardPanel, +} from './embeddable_saved_object_converters'; +import { SavedDashboardPanel } from '../types'; +import { SavedObjectReference } from '../../../../core/types'; +import { EmbeddablePersistableStateService } from '../../../embeddable/common/types'; + +export interface InjectDeps { + embeddablePersistableStateService: EmbeddablePersistableStateService; +} + +export function injectPanelsReferences( + panels: SavedDashboardPanel[], + references: SavedObjectReference[], + deps: InjectDeps +): SavedDashboardPanel[] { + const result: SavedDashboardPanel[] = []; + for (const panel of panels) { + const embeddableState = convertSavedDashboardPanelToPanelState(panel); + embeddableState.explicitInput = omit( + deps.embeddablePersistableStateService.inject( + { ...embeddableState.explicitInput, type: panel.type }, + references + ), + 'type' + ); + result.push(convertPanelStateToSavedDashboardPanel(embeddableState, panel.version)); + } + return result; +} + +export interface ExtractDeps { + embeddablePersistableStateService: EmbeddablePersistableStateService; +} + +export function extractPanelsReferences( + panels: SavedDashboardPanel[], + deps: ExtractDeps +): Array<{ panel: SavedDashboardPanel; references: SavedObjectReference[] }> { + const result: Array<{ panel: SavedDashboardPanel; references: SavedObjectReference[] }> = []; + + for (const panel of panels) { + const embeddable = convertSavedDashboardPanelToPanelState(panel); + const { + state: embeddableInputWithExtractedReferences, + references, + } = deps.embeddablePersistableStateService.extract({ + ...embeddable.explicitInput, + type: embeddable.type, + }); + embeddable.explicitInput = omit(embeddableInputWithExtractedReferences, 'type'); + + const newPanel = convertPanelStateToSavedDashboardPanel(embeddable, panel.version); + result.push({ + panel: newPanel, + references, + }); + } + + return result; +} diff --git a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.test.ts b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.test.ts similarity index 82% rename from src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.test.ts rename to src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.test.ts index 926d5f405b384a..bf044a1fa77d10 100644 --- a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.test.ts +++ b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.test.ts @@ -21,9 +21,8 @@ import { convertSavedDashboardPanelToPanelState, convertPanelStateToSavedDashboardPanel, } from './embeddable_saved_object_converters'; -import { SavedDashboardPanel } from '../../types'; -import { DashboardPanelState } from '../embeddable'; -import { EmbeddableInput } from '../../../../embeddable/public'; +import { SavedDashboardPanel, DashboardPanelState } from '../types'; +import { EmbeddableInput } from '../../../embeddable/common/types'; test('convertSavedDashboardPanelToPanelState', () => { const savedDashboardPanel: SavedDashboardPanel = { @@ -135,3 +134,24 @@ test('convertPanelStateToSavedDashboardPanel will not add an undefined id when n const converted = convertPanelStateToSavedDashboardPanel(dashboardPanel, '8.0.0'); expect(converted.hasOwnProperty('id')).toBe(false); }); + +test('convertPanelStateToSavedDashboardPanel will not leave title as part of embeddable config', () => { + const dashboardPanel: DashboardPanelState = { + gridData: { + x: 0, + y: 0, + h: 15, + w: 15, + i: '123', + }, + explicitInput: { + id: '123', + title: 'title', + } as EmbeddableInput, + type: 'search', + }; + + const converted = convertPanelStateToSavedDashboardPanel(dashboardPanel, '8.0.0'); + expect(converted.embeddableConfig.hasOwnProperty('title')).toBe(false); + expect(converted.title).toBe('title'); +}); diff --git a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.ts b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts similarity index 91% rename from src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.ts rename to src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts index b19ef31ccb9ac0..b71b4f067ae33d 100644 --- a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.ts +++ b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts @@ -17,9 +17,8 @@ * under the License. */ import { omit } from 'lodash'; -import { SavedDashboardPanel } from '../../types'; -import { DashboardPanelState } from '../embeddable'; -import { SavedObjectEmbeddableInput } from '../../embeddable_plugin'; +import { DashboardPanelState, SavedDashboardPanel } from '../types'; +import { SavedObjectEmbeddableInput } from '../../../embeddable/common/'; export function convertSavedDashboardPanelToPanelState( savedDashboardPanel: SavedDashboardPanel @@ -49,7 +48,7 @@ export function convertPanelStateToSavedDashboardPanel( type: panelState.type, gridData: panelState.gridData, panelIndex: panelState.explicitInput.id, - embeddableConfig: omit(panelState.explicitInput, ['id', 'savedObjectId']), + embeddableConfig: omit(panelState.explicitInput, ['id', 'savedObjectId', 'title']), ...(customTitle && { title: customTitle }), ...(savedObjectId !== undefined && { id: savedObjectId }), }; diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.test.ts b/src/plugins/dashboard/common/saved_dashboard_references.test.ts similarity index 52% rename from src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.test.ts rename to src/plugins/dashboard/common/saved_dashboard_references.test.ts index 48f15e84c93072..3632c4cca9e937 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.test.ts +++ b/src/plugins/dashboard/common/saved_dashboard_references.test.ts @@ -17,8 +17,18 @@ * under the License. */ -import { extractReferences, injectReferences } from './saved_dashboard_references'; -import { SavedObjectDashboard } from './saved_dashboard'; +import { + extractReferences, + injectReferences, + InjectDeps, + ExtractDeps, +} from './saved_dashboard_references'; +import { createEmbeddablePersistableStateServiceMock } from '../../embeddable/common/mocks'; + +const embeddablePersistableStateServiceMock = createEmbeddablePersistableStateServiceMock(); +const deps: InjectDeps & ExtractDeps = { + embeddablePersistableStateService: embeddablePersistableStateServiceMock, +}; describe('extractReferences', () => { test('extracts references from panelsJSON', () => { @@ -41,28 +51,28 @@ describe('extractReferences', () => { }, references: [], }; - const updatedDoc = extractReferences(doc); + const updatedDoc = extractReferences(doc, deps); expect(updatedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"panelRefName\\":\\"panel_0\\"},{\\"title\\":\\"Title 2\\",\\"panelRefName\\":\\"panel_1\\"}]", - }, - "references": Array [ - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], -} -`); + Object { + "attributes": Object { + "foo": true, + "panelsJSON": "[{\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"panelRefName\\":\\"panel_0\\"},{\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"panelRefName\\":\\"panel_1\\"}]", + }, + "references": Array [ + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + } + `); }); test('fails when "type" attribute is missing from a panel', () => { @@ -79,7 +89,7 @@ Object { }, references: [], }; - expect(() => extractReferences(doc)).toThrowErrorMatchingInlineSnapshot( + expect(() => extractReferences(doc, deps)).toThrowErrorMatchingInlineSnapshot( `"\\"type\\" attribute is missing from panel \\"0\\""` ); }); @@ -98,21 +108,21 @@ Object { }, references: [], }; - expect(extractReferences(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"title\\":\\"Title 1\\"}]", - }, - "references": Array [], -} -`); + expect(extractReferences(doc, deps)).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "foo": true, + "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\"}]", + }, + "references": Array [], + } + `); }); }); describe('injectReferences', () => { - test('injects references into context', () => { - const context = { + test('returns injected attributes', () => { + const attributes = { id: '1', title: 'test', panelsJSON: JSON.stringify([ @@ -125,7 +135,7 @@ describe('injectReferences', () => { title: 'Title 2', }, ]), - } as SavedObjectDashboard; + }; const references = [ { name: 'panel_0', @@ -138,49 +148,49 @@ describe('injectReferences', () => { id: '2', }, ]; - injectReferences(context, references); + const newAttributes = injectReferences({ attributes, references }, deps); - expect(context).toMatchInlineSnapshot(` -Object { - "id": "1", - "panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\",\\"type\\":\\"visualization\\"},{\\"title\\":\\"Title 2\\",\\"id\\":\\"2\\",\\"type\\":\\"visualization\\"}]", - "title": "test", -} -`); + expect(newAttributes).toMatchInlineSnapshot(` + Object { + "id": "1", + "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"id\\":\\"2\\"}]", + "title": "test", + } + `); }); test('skips when panelsJSON is missing', () => { - const context = { + const attributes = { id: '1', title: 'test', - } as SavedObjectDashboard; - injectReferences(context, []); - expect(context).toMatchInlineSnapshot(` -Object { - "id": "1", - "title": "test", -} -`); + }; + const newAttributes = injectReferences({ attributes, references: [] }, deps); + expect(newAttributes).toMatchInlineSnapshot(` + Object { + "id": "1", + "title": "test", + } + `); }); test('skips when panelsJSON is not an array', () => { - const context = { + const attributes = { id: '1', panelsJSON: '{}', title: 'test', - } as SavedObjectDashboard; - injectReferences(context, []); - expect(context).toMatchInlineSnapshot(` -Object { - "id": "1", - "panelsJSON": "{}", - "title": "test", -} -`); + }; + const newAttributes = injectReferences({ attributes, references: [] }, deps); + expect(newAttributes).toMatchInlineSnapshot(` + Object { + "id": "1", + "panelsJSON": "{}", + "title": "test", + } + `); }); test('skips a panel when panelRefName is missing', () => { - const context = { + const attributes = { id: '1', title: 'test', panelsJSON: JSON.stringify([ @@ -192,7 +202,7 @@ Object { title: 'Title 2', }, ]), - } as SavedObjectDashboard; + }; const references = [ { name: 'panel_0', @@ -200,18 +210,18 @@ Object { id: '1', }, ]; - injectReferences(context, references); - expect(context).toMatchInlineSnapshot(` -Object { - "id": "1", - "panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\",\\"type\\":\\"visualization\\"},{\\"title\\":\\"Title 2\\"}]", - "title": "test", -} -`); + const newAttributes = injectReferences({ attributes, references }, deps); + expect(newAttributes).toMatchInlineSnapshot(` + Object { + "id": "1", + "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\"}]", + "title": "test", + } + `); }); test(`fails when it can't find the reference in the array`, () => { - const context = { + const attributes = { id: '1', title: 'test', panelsJSON: JSON.stringify([ @@ -220,9 +230,9 @@ Object { title: 'Title 1', }, ]), - } as SavedObjectDashboard; - expect(() => injectReferences(context, [])).toThrowErrorMatchingInlineSnapshot( - `"Could not find reference \\"panel_0\\""` - ); + }; + expect(() => + injectReferences({ attributes, references: [] }, deps) + ).toThrowErrorMatchingInlineSnapshot(`"Could not find reference \\"panel_0\\""`); }); }); diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.ts b/src/plugins/dashboard/common/saved_dashboard_references.ts similarity index 55% rename from src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.ts rename to src/plugins/dashboard/common/saved_dashboard_references.ts index 3df9e64887725a..0726d301b34ac4 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.ts +++ b/src/plugins/dashboard/common/saved_dashboard_references.ts @@ -17,18 +17,47 @@ * under the License. */ -import { SavedObjectAttributes, SavedObjectReference } from 'kibana/public'; -import { SavedObjectDashboard } from './saved_dashboard'; +import { SavedObjectAttributes, SavedObjectReference } from '../../../core/types'; +import { + extractPanelsReferences, + injectPanelsReferences, +} from './embeddable/embeddable_references'; +import { SavedDashboardPanel730ToLatest } from './types'; +import { EmbeddablePersistableStateService } from '../../embeddable/common/types'; -export function extractReferences({ - attributes, - references = [], -}: { +export interface ExtractDeps { + embeddablePersistableStateService: EmbeddablePersistableStateService; +} + +export interface SavedObjectAttributesAndReferences { attributes: SavedObjectAttributes; references: SavedObjectReference[]; -}) { +} + +export function extractReferences( + { attributes, references = [] }: SavedObjectAttributesAndReferences, + deps: ExtractDeps +): SavedObjectAttributesAndReferences { + if (typeof attributes.panelsJSON !== 'string') { + return { attributes, references }; + } const panelReferences: SavedObjectReference[] = []; - const panels: Array> = JSON.parse(String(attributes.panelsJSON)); + let panels: Array> = JSON.parse(String(attributes.panelsJSON)); + + const extractedReferencesResult = extractPanelsReferences( + (panels as unknown) as SavedDashboardPanel730ToLatest[], + deps + ); + + panels = (extractedReferencesResult.map((res) => res.panel) as unknown) as Array< + Record + >; + extractedReferencesResult.forEach((res) => { + panelReferences.push(...res.references); + }); + + // TODO: This extraction should be done by EmbeddablePersistableStateService + // https://github.com/elastic/kibana/issues/82830 panels.forEach((panel, i) => { if (!panel.type) { throw new Error(`"type" attribute is missing from panel "${i}"`); @@ -46,6 +75,7 @@ export function extractReferences({ delete panel.type; delete panel.id; }); + return { references: [...references, ...panelReferences], attributes: { @@ -55,21 +85,28 @@ export function extractReferences({ }; } +export interface InjectDeps { + embeddablePersistableStateService: EmbeddablePersistableStateService; +} + export function injectReferences( - savedObject: SavedObjectDashboard, - references: SavedObjectReference[] -) { + { attributes, references = [] }: SavedObjectAttributesAndReferences, + deps: InjectDeps +): SavedObjectAttributes { // Skip if panelsJSON is missing otherwise this will cause saved object import to fail when // importing objects without panelsJSON. At development time of this, there is no guarantee each saved // object has panelsJSON in all previous versions of kibana. - if (typeof savedObject.panelsJSON !== 'string') { - return; + if (typeof attributes.panelsJSON !== 'string') { + return attributes; } - const panels = JSON.parse(savedObject.panelsJSON); + let panels = JSON.parse(attributes.panelsJSON); // Same here, prevent failing saved object import if ever panels aren't an array. if (!Array.isArray(panels)) { - return; + return attributes; } + + // TODO: This injection should be done by EmbeddablePersistableStateService + // https://github.com/elastic/kibana/issues/82830 panels.forEach((panel) => { if (!panel.panelRefName) { return; @@ -84,5 +121,11 @@ export function injectReferences( panel.type = reference.type; delete panel.panelRefName; }); - savedObject.panelsJSON = JSON.stringify(panels); + + panels = injectPanelsReferences(panels, references, deps); + + return { + ...attributes, + panelsJSON: JSON.stringify(panels), + }; } diff --git a/src/plugins/dashboard/common/types.ts b/src/plugins/dashboard/common/types.ts index 7cc82a91739767..ae214764052dca 100644 --- a/src/plugins/dashboard/common/types.ts +++ b/src/plugins/dashboard/common/types.ts @@ -17,6 +17,8 @@ * under the License. */ +import { EmbeddableInput, PanelState } from '../../../../src/plugins/embeddable/common/types'; +import { SavedObjectEmbeddableInput } from '../../../../src/plugins/embeddable/common/lib/saved_object_embeddable'; import { RawSavedDashboardPanelTo60, RawSavedDashboardPanel610, @@ -26,6 +28,21 @@ import { RawSavedDashboardPanel730ToLatest, } from './bwc/types'; +import { GridData } from './embeddable/types'; +export type PanelId = string; +export type SavedObjectId = string; + +export interface DashboardPanelState< + TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput +> extends PanelState { + readonly gridData: GridData; +} + +/** + * This should always represent the latest dashboard panel shape, after all possible migrations. + */ +export type SavedDashboardPanel = SavedDashboardPanel730ToLatest; + export type SavedDashboardPanel640To720 = Pick< RawSavedDashboardPanel640To720, Exclude diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index feae110c271fc6..c99e4e4e069878 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -81,7 +81,6 @@ import { getTopNavConfig } from './top_nav/get_top_nav_config'; import { TopNavIds } from './top_nav/top_nav_ids'; import { getDashboardTitle } from './dashboard_strings'; import { DashboardAppScope } from './dashboard_app'; -import { convertSavedDashboardPanelToPanelState } from './lib/embeddable_saved_object_converters'; import { RenderDeps } from './application'; import { IKbnUrlStateStorage, @@ -97,6 +96,7 @@ import { subscribeWithScope, } from '../../../kibana_legacy/public'; import { migrateLegacyQuery } from './lib/migrate_legacy_query'; +import { convertSavedDashboardPanelToPanelState } from '../../common/embeddable/embeddable_saved_object_converters'; export interface DashboardAppControllerDependencies extends RenderDeps { $scope: DashboardAppScope; diff --git a/src/plugins/dashboard/public/application/dashboard_state_manager.ts b/src/plugins/dashboard/public/application/dashboard_state_manager.ts index 38479b1384477c..6ef109ff60e421 100644 --- a/src/plugins/dashboard/public/application/dashboard_state_manager.ts +++ b/src/plugins/dashboard/public/application/dashboard_state_manager.ts @@ -30,7 +30,6 @@ import { migrateLegacyQuery } from './lib/migrate_legacy_query'; import { ViewMode } from '../embeddable_plugin'; import { getAppStateDefaults, migrateAppState, getDashboardIdFromUrl } from './lib'; -import { convertPanelStateToSavedDashboardPanel } from './lib/embeddable_saved_object_converters'; import { FilterUtils } from './lib/filter_utils'; import { DashboardAppState, @@ -48,6 +47,7 @@ import { } from '../../../kibana_utils/public'; import { SavedObjectDashboard } from '../saved_dashboards'; import { DashboardContainer } from './embeddable'; +import { convertPanelStateToSavedDashboardPanel } from '../../common/embeddable/embeddable_saved_object_converters'; /** * Dashboard state manager handles connecting angular and redux state between the angular and react portions of the diff --git a/src/plugins/dashboard/public/application/embeddable/types.ts b/src/plugins/dashboard/public/application/embeddable/types.ts index 66cdd22ed6bd43..efeb68c8a885ab 100644 --- a/src/plugins/dashboard/public/application/embeddable/types.ts +++ b/src/plugins/dashboard/public/application/embeddable/types.ts @@ -16,14 +16,4 @@ * specific language governing permissions and limitations * under the License. */ -import { SavedObjectEmbeddableInput } from 'src/plugins/embeddable/public'; -import { GridData } from '../../../common'; -import { PanelState, EmbeddableInput } from '../../embeddable_plugin'; -export type PanelId = string; -export type SavedObjectId = string; - -export interface DashboardPanelState< - TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput -> extends PanelState { - readonly gridData: GridData; -} +export * from '../../../common/types'; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 53b892475704f9..24bf736cfa2740 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -450,6 +450,7 @@ export class DashboardPlugin const savedDashboardLoader = createSavedDashboardLoader({ savedObjectsClient: core.savedObjects.client, savedObjects: plugins.savedObjects, + embeddableStart: plugins.embeddable, }); const dashboardContainerFactory = plugins.embeddable.getEmbeddableFactory( DASHBOARD_CONTAINER_TYPE diff --git a/src/plugins/dashboard/public/saved_dashboards/index.ts b/src/plugins/dashboard/public/saved_dashboards/index.ts index 9b7745bd884f71..9adaf0dc3ba15f 100644 --- a/src/plugins/dashboard/public/saved_dashboards/index.ts +++ b/src/plugins/dashboard/public/saved_dashboards/index.ts @@ -16,6 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -export * from './saved_dashboard_references'; +export * from '../../common/saved_dashboard_references'; export * from './saved_dashboard'; export * from './saved_dashboards'; diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts index bfc52ec33c35c7..e3bfe346fbc07b 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts +++ b/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts @@ -17,10 +17,12 @@ * under the License. */ import { SavedObject, SavedObjectsStart } from '../../../../plugins/saved_objects/public'; -import { extractReferences, injectReferences } from './saved_dashboard_references'; import { Filter, ISearchSource, Query, RefreshInterval } from '../../../../plugins/data/public'; import { createDashboardEditUrl } from '../dashboard_constants'; +import { EmbeddableStart } from '../../../embeddable/public'; +import { SavedObjectAttributes, SavedObjectReference } from '../../../../core/types'; +import { extractReferences, injectReferences } from '../../common/saved_dashboard_references'; export interface SavedObjectDashboard extends SavedObject { id?: string; @@ -41,7 +43,8 @@ export interface SavedObjectDashboard extends SavedObject { // Used only by the savedDashboards service, usually no reason to change this export function createSavedDashboardClass( - savedObjectStart: SavedObjectsStart + savedObjectStart: SavedObjectsStart, + embeddableStart: EmbeddableStart ): new (id: string) => SavedObjectDashboard { class SavedDashboard extends savedObjectStart.SavedObjectClass { // save these objects with the 'dashboard' type @@ -77,8 +80,19 @@ export function createSavedDashboardClass( type: SavedDashboard.type, mapping: SavedDashboard.mapping, searchSource: SavedDashboard.searchSource, - extractReferences, - injectReferences, + extractReferences: (opts: { + attributes: SavedObjectAttributes; + references: SavedObjectReference[]; + }) => extractReferences(opts, { embeddablePersistableStateService: embeddableStart }), + injectReferences: (so: SavedObjectDashboard, references: SavedObjectReference[]) => { + const newAttributes = injectReferences( + { attributes: so._serialize().attributes, references }, + { + embeddablePersistableStateService: embeddableStart, + } + ); + Object.assign(so, newAttributes); + }, // if this is null/undefined then the SavedObject will be assigned the defaults id, diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts index 750fec4d4d1f91..7193a77fd0ec9c 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts +++ b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts @@ -20,16 +20,22 @@ import { SavedObjectsClientContract } from 'kibana/public'; import { SavedObjectLoader, SavedObjectsStart } from '../../../../plugins/saved_objects/public'; import { createSavedDashboardClass } from './saved_dashboard'; +import { EmbeddableStart } from '../../../embeddable/public'; interface Services { savedObjectsClient: SavedObjectsClientContract; savedObjects: SavedObjectsStart; + embeddableStart: EmbeddableStart; } /** * @param services */ -export function createSavedDashboardLoader({ savedObjects, savedObjectsClient }: Services) { - const SavedDashboard = createSavedDashboardClass(savedObjects); +export function createSavedDashboardLoader({ + savedObjects, + savedObjectsClient, + embeddableStart, +}: Services) { + const SavedDashboard = createSavedDashboardClass(savedObjects, embeddableStart); return new SavedObjectLoader(SavedDashboard, savedObjectsClient); } diff --git a/src/plugins/dashboard/public/types.ts b/src/plugins/dashboard/public/types.ts index 1af739c34b76a4..8f6fe7fce5cfe5 100644 --- a/src/plugins/dashboard/public/types.ts +++ b/src/plugins/dashboard/public/types.ts @@ -19,9 +19,12 @@ import { Query, Filter } from 'src/plugins/data/public'; import { SavedObject as SavedObjectType, SavedObjectAttributes } from 'src/core/public'; -import { SavedDashboardPanel730ToLatest } from '../common'; + import { ViewMode } from './embeddable_plugin'; +import { SavedDashboardPanel } from '../common/types'; +export { SavedDashboardPanel }; + export interface DashboardCapabilities { showWriteControls: boolean; createNew: boolean; @@ -71,11 +74,6 @@ export interface Field { export type NavAction = (anchorElement?: any) => void; -/** - * This should always represent the latest dashboard panel shape, after all possible migrations. - */ -export type SavedDashboardPanel = SavedDashboardPanel730ToLatest; - export interface DashboardAppState { panels: SavedDashboardPanel[]; fullScreenMode: boolean; diff --git a/src/plugins/dashboard/server/plugin.ts b/src/plugins/dashboard/server/plugin.ts index ba7bdeeda01335..6a4c297f258819 100644 --- a/src/plugins/dashboard/server/plugin.ts +++ b/src/plugins/dashboard/server/plugin.ts @@ -25,22 +25,34 @@ import { Logger, } from '../../../core/server'; -import { dashboardSavedObjectType } from './saved_objects'; +import { createDashboardSavedObjectType } from './saved_objects'; import { capabilitiesProvider } from './capabilities_provider'; import { DashboardPluginSetup, DashboardPluginStart } from './types'; +import { EmbeddableSetup } from '../../embeddable/server'; -export class DashboardPlugin implements Plugin { +interface SetupDeps { + embeddable: EmbeddableSetup; +} + +export class DashboardPlugin + implements Plugin { private readonly logger: Logger; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); } - public setup(core: CoreSetup) { + public setup(core: CoreSetup, plugins: SetupDeps) { this.logger.debug('dashboard: Setup'); - core.savedObjects.registerType(dashboardSavedObjectType); + core.savedObjects.registerType( + createDashboardSavedObjectType({ + migrationDeps: { + embeddable: plugins.embeddable, + }, + }) + ); core.capabilities.registerProvider(capabilitiesProvider); return {}; diff --git a/src/plugins/dashboard/server/saved_objects/dashboard.ts b/src/plugins/dashboard/server/saved_objects/dashboard.ts index a85f67f5ba56ad..7d3e48ce1ae8b0 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard.ts @@ -18,9 +18,16 @@ */ import { SavedObjectsType } from 'kibana/server'; -import { dashboardSavedObjectTypeMigrations } from './dashboard_migrations'; +import { + createDashboardSavedObjectTypeMigrations, + DashboardSavedObjectTypeMigrationsDeps, +} from './dashboard_migrations'; -export const dashboardSavedObjectType: SavedObjectsType = { +export const createDashboardSavedObjectType = ({ + migrationDeps, +}: { + migrationDeps: DashboardSavedObjectTypeMigrationsDeps; +}): SavedObjectsType => ({ name: 'dashboard', hidden: false, namespaceType: 'single', @@ -65,5 +72,5 @@ export const dashboardSavedObjectType: SavedObjectsType = { version: { type: 'integer' }, }, }, - migrations: dashboardSavedObjectTypeMigrations, -}; + migrations: createDashboardSavedObjectTypeMigrations(migrationDeps), +}); diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts index 22ed18f75c652a..50f12d21d4db93 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts @@ -19,7 +19,14 @@ import { SavedObjectUnsanitizedDoc } from 'kibana/server'; import { savedObjectsServiceMock } from '../../../../core/server/mocks'; -import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations'; +import { createEmbeddableSetupMock } from '../../../embeddable/server/mocks'; +import { createDashboardSavedObjectTypeMigrations } from './dashboard_migrations'; +import { DashboardDoc730ToLatest } from '../../common'; + +const embeddableSetupMock = createEmbeddableSetupMock(); +const migrations = createDashboardSavedObjectTypeMigrations({ + embeddable: embeddableSetupMock, +}); const contextMock = savedObjectsServiceMock.createMigrationContext(); @@ -448,4 +455,50 @@ Object { `); }); }); + + describe('7.11.0 - embeddable persistable state extraction', () => { + const migration = migrations['7.11.0']; + const doc: DashboardDoc730ToLatest = { + attributes: { + description: '', + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"query":{"language":"kuery","query":""},"filter":[{"query":{"match_phrase":{"machine.os.keyword":"osx"}},"$state":{"store":"appState"},"meta":{"type":"phrase","key":"machine.os.keyword","params":{"query":"osx"},"disabled":false,"negate":false,"alias":null,"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index"}}]}', + }, + optionsJSON: '{"useMargins":true,"hidePanelTitles":false}', + panelsJSON: + '[{"version":"7.9.3","gridData":{"x":0,"y":0,"w":24,"h":15,"i":"82fa0882-9f9e-476a-bbb9-03555e5ced91"},"panelIndex":"82fa0882-9f9e-476a-bbb9-03555e5ced91","embeddableConfig":{"enhancements":{"dynamicActions":{"events":[]}}},"panelRefName":"panel_0"}]', + timeRestore: false, + title: 'Dashboard A', + version: 1, + }, + id: '376e6260-1f5e-11eb-91aa-7b6d5f8a61d6', + references: [ + { + id: '90943e30-9a47-11e8-b64d-95841ca0b247', + name: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', + type: 'index-pattern', + }, + { id: '14e2e710-4258-11e8-b3aa-73fdaf54bfc9', name: 'panel_0', type: 'visualization' }, + ], + type: 'dashboard', + }; + + test('should migrate 7.3.0 doc without embeddable state to extract', () => { + const newDoc = migration(doc, contextMock); + expect(newDoc).toEqual(doc); + }); + + test('should migrate 7.3.0 doc and extract embeddable state', () => { + embeddableSetupMock.extract.mockImplementationOnce((state) => ({ + state: { ...state, __extracted: true }, + references: [{ id: '__new', name: '__newRefName', type: '__newType' }], + })); + + const newDoc = migration(doc, contextMock); + expect(newDoc).not.toEqual(doc); + expect(newDoc.references).toHaveLength(doc.references.length + 1); + expect(JSON.parse(newDoc.attributes.panelsJSON)[0].embeddableConfig.__extracted).toBe(true); + }); + }); }); diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts index ac91c5a92048a6..177440c5ea5d11 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts @@ -18,11 +18,12 @@ */ import { get, flow } from 'lodash'; - -import { SavedObjectMigrationFn } from 'kibana/server'; +import { SavedObjectAttributes, SavedObjectMigrationFn } from 'kibana/server'; import { migrations730 } from './migrations_730'; import { migrateMatchAllQuery } from './migrate_match_all_query'; -import { DashboardDoc700To720 } from '../../common'; +import { DashboardDoc700To720, DashboardDoc730ToLatest } from '../../common'; +import { EmbeddableSetup } from '../../../embeddable/server'; +import { injectReferences, extractReferences } from '../../common/saved_dashboard_references'; function migrateIndexPattern(doc: DashboardDoc700To720) { const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); @@ -100,7 +101,57 @@ const migrations700: SavedObjectMigrationFn = (doc): DashboardDoc700To return doc as DashboardDoc700To720; }; -export const dashboardSavedObjectTypeMigrations = { +/** + * In 7.8.0 we introduced dashboard drilldowns which are stored inside dashboard saved object as part of embeddable state + * In 7.11.0 we created an embeddable references/migrations system that allows to properly extract embeddable persistable state + * https://github.com/elastic/kibana/issues/71409 + * The idea of this migration is to inject all the embeddable panel references and then run the extraction again. + * As the result of the extraction: + * 1. In addition to regular `panel_` we will get new references which are extracted by `embeddablePersistableStateService` (dashboard drilldown references) + * 2. `panel_` references will be regenerated + * All other references like index-patterns are forwarded non touched + * @param deps + */ +function createExtractPanelReferencesMigration( + deps: DashboardSavedObjectTypeMigrationsDeps +): SavedObjectMigrationFn { + return (doc) => { + const references = doc.references ?? []; + + /** + * Remembering this because dashboard's extractReferences won't return those + * All other references like `panel_` will be overwritten + */ + const oldNonPanelReferences = references.filter((ref) => !ref.name.startsWith('panel_')); + + const injectedAttributes = injectReferences( + { + attributes: (doc.attributes as unknown) as SavedObjectAttributes, + references, + }, + { embeddablePersistableStateService: deps.embeddable } + ); + + const { attributes, references: newPanelReferences } = extractReferences( + { attributes: injectedAttributes, references: [] }, + { embeddablePersistableStateService: deps.embeddable } + ); + + return { + ...doc, + references: [...oldNonPanelReferences, ...newPanelReferences], + attributes, + }; + }; +} + +export interface DashboardSavedObjectTypeMigrationsDeps { + embeddable: EmbeddableSetup; +} + +export const createDashboardSavedObjectTypeMigrations = ( + deps: DashboardSavedObjectTypeMigrationsDeps +) => ({ /** * We need to have this migration twice, once with a version prior to 7.0.0 once with a version * after it. The reason for that is, that this migration has been introduced once 7.0.0 was already @@ -115,4 +166,5 @@ export const dashboardSavedObjectTypeMigrations = { '7.0.0': flow(migrations700), '7.3.0': flow(migrations730), '7.9.3': flow(migrateMatchAllQuery), -}; + '7.11.0': flow(createExtractPanelReferencesMigration(deps)), +}); diff --git a/src/plugins/dashboard/server/saved_objects/index.ts b/src/plugins/dashboard/server/saved_objects/index.ts index ca97b9d2a6b70f..ea4808de968480 100644 --- a/src/plugins/dashboard/server/saved_objects/index.ts +++ b/src/plugins/dashboard/server/saved_objects/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { dashboardSavedObjectType } from './dashboard'; +export { createDashboardSavedObjectType } from './dashboard'; diff --git a/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts b/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts index a58df547fa5224..37a8881ab520b7 100644 --- a/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts +++ b/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts @@ -18,12 +18,16 @@ */ import { savedObjectsServiceMock } from '../../../../core/server/mocks'; -import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations'; +import { createDashboardSavedObjectTypeMigrations } from './dashboard_migrations'; import { migrations730 } from './migrations_730'; import { DashboardDoc700To720, DashboardDoc730ToLatest, DashboardDocPre700 } from '../../common'; import { RawSavedDashboardPanel730ToLatest } from '../../common'; +import { createEmbeddableSetupMock } from '../../../embeddable/server/mocks'; const mockContext = savedObjectsServiceMock.createMigrationContext(); +const migrations = createDashboardSavedObjectTypeMigrations({ + embeddable: createEmbeddableSetupMock(), +}); test('dashboard migration 7.3.0 migrates filters to query on search source', () => { const doc: DashboardDoc700To720 = { diff --git a/src/plugins/embeddable/common/index.ts b/src/plugins/embeddable/common/index.ts new file mode 100644 index 00000000000000..a4cbfb11b36f85 --- /dev/null +++ b/src/plugins/embeddable/common/index.ts @@ -0,0 +1,21 @@ +/* + * 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 * from './types'; +export * from './lib'; diff --git a/src/plugins/embeddable/common/lib/index.ts b/src/plugins/embeddable/common/lib/index.ts index e180ca9489df05..1ac6834365cd13 100644 --- a/src/plugins/embeddable/common/lib/index.ts +++ b/src/plugins/embeddable/common/lib/index.ts @@ -22,3 +22,4 @@ export * from './inject'; export * from './migrate'; export * from './migrate_base_input'; export * from './telemetry'; +export * from './saved_object_embeddable'; diff --git a/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts b/src/plugins/embeddable/common/lib/saved_object_embeddable.ts similarity index 96% rename from src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts rename to src/plugins/embeddable/common/lib/saved_object_embeddable.ts index 5f093c55e94e4f..f2dc9ed1ae3956 100644 --- a/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts +++ b/src/plugins/embeddable/common/lib/saved_object_embeddable.ts @@ -17,7 +17,7 @@ * under the License. */ -import { EmbeddableInput } from '..'; +import { EmbeddableInput } from '../types'; export interface SavedObjectEmbeddableInput extends EmbeddableInput { savedObjectId: string; diff --git a/src/plugins/embeddable/common/mocks.ts b/src/plugins/embeddable/common/mocks.ts new file mode 100644 index 00000000000000..a9ac144d1f2762 --- /dev/null +++ b/src/plugins/embeddable/common/mocks.ts @@ -0,0 +1,31 @@ +/* + * 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 { EmbeddablePersistableStateService } from './types'; + +export const createEmbeddablePersistableStateServiceMock = (): jest.Mocked< + EmbeddablePersistableStateService +> => { + return { + inject: jest.fn((state, references) => state), + extract: jest.fn((state) => ({ state, references: [] })), + migrate: jest.fn((state, version) => state), + telemetry: jest.fn((state, collector) => ({})), + }; +}; diff --git a/src/plugins/embeddable/common/types.ts b/src/plugins/embeddable/common/types.ts index 7e024eda9b7937..8965446cc85fa7 100644 --- a/src/plugins/embeddable/common/types.ts +++ b/src/plugins/embeddable/common/types.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SerializableState } from '../../kibana_utils/common'; +import { PersistableStateService, SerializableState } from '../../kibana_utils/common'; import { Query, TimeRange } from '../../data/common/query'; import { Filter } from '../../data/common/es_query/filters'; @@ -74,8 +74,21 @@ export type EmbeddableInput = { searchSessionId?: string; }; +export interface PanelState { + // The type of embeddable in this panel. Will be used to find the factory in which to + // load the embeddable. + type: string; + + // Stores input for this embeddable that is specific to this embeddable. Other parts of embeddable input + // will be derived from the container's input. **Any state in here will override any state derived from + // the container.** + explicitInput: Partial & { id: string }; +} + export type EmbeddableStateWithType = EmbeddableInput & { type: string }; +export type EmbeddablePersistableStateService = PersistableStateService; + export interface CommonEmbeddableStartContract { getEmbeddableFactory: (embeddableFactoryId: string) => any; getEnhancement: (enhancementId: string) => any; diff --git a/src/plugins/embeddable/public/lib/containers/container.ts b/src/plugins/embeddable/public/lib/containers/container.ts index 4dede8bf5d752f..a5c5133dbc7028 100644 --- a/src/plugins/embeddable/public/lib/containers/container.ts +++ b/src/plugins/embeddable/public/lib/containers/container.ts @@ -31,7 +31,7 @@ import { import { IContainer, ContainerInput, ContainerOutput, PanelState } from './i_container'; import { PanelNotFoundError, EmbeddableFactoryNotFoundError } from '../errors'; import { EmbeddableStart } from '../../plugin'; -import { isSavedObjectEmbeddableInput } from '../embeddables/saved_object_embeddable'; +import { isSavedObjectEmbeddableInput } from '../../../common/lib/saved_object_embeddable'; const getKeys = (o: T): Array => Object.keys(o) as Array; diff --git a/src/plugins/embeddable/public/lib/containers/i_container.ts b/src/plugins/embeddable/public/lib/containers/i_container.ts index db219fa8b73142..270caec2f3f843 100644 --- a/src/plugins/embeddable/public/lib/containers/i_container.ts +++ b/src/plugins/embeddable/public/lib/containers/i_container.ts @@ -24,17 +24,9 @@ import { ErrorEmbeddable, IEmbeddable, } from '../embeddables'; +import { PanelState } from '../../../common/types'; -export interface PanelState { - // The type of embeddable in this panel. Will be used to find the factory in which to - // load the embeddable. - type: string; - - // Stores input for this embeddable that is specific to this embeddable. Other parts of embeddable input - // will be derived from the container's input. **Any state in here will override any state derived from - // the container.** - explicitInput: Partial & { id: string }; -} +export { PanelState }; export interface ContainerOutput extends EmbeddableOutput { embeddableLoaded: { [key: string]: boolean }; diff --git a/src/plugins/embeddable/public/lib/embeddables/index.ts b/src/plugins/embeddable/public/lib/embeddables/index.ts index 5bab5ac27f3cc9..2f6de1be60c9c4 100644 --- a/src/plugins/embeddable/public/lib/embeddables/index.ts +++ b/src/plugins/embeddable/public/lib/embeddables/index.ts @@ -24,5 +24,5 @@ export * from './default_embeddable_factory_provider'; export { ErrorEmbeddable, isErrorEmbeddable } from './error_embeddable'; export { withEmbeddableSubscription } from './with_subscription'; export { EmbeddableRoot } from './embeddable_root'; -export * from './saved_object_embeddable'; +export * from '../../../common/lib/saved_object_embeddable'; export { EmbeddableRenderer, EmbeddableRendererProps } from './embeddable_renderer'; diff --git a/src/plugins/embeddable/server/mocks.ts b/src/plugins/embeddable/server/mocks.ts new file mode 100644 index 00000000000000..28bb9542ab7cb0 --- /dev/null +++ b/src/plugins/embeddable/server/mocks.ts @@ -0,0 +1,30 @@ +/* + * 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 { createEmbeddablePersistableStateServiceMock } from '../common/mocks'; +import { EmbeddableSetup, EmbeddableStart } from './plugin'; + +export const createEmbeddableSetupMock = (): jest.Mocked => ({ + ...createEmbeddablePersistableStateServiceMock(), + registerEmbeddableFactory: jest.fn(), + registerEnhancement: jest.fn(), +}); + +export const createEmbeddableStartMock = (): jest.Mocked => + createEmbeddablePersistableStateServiceMock(); diff --git a/src/plugins/embeddable/server/plugin.ts b/src/plugins/embeddable/server/plugin.ts index 6e9186e2864910..d99675f950ad0b 100644 --- a/src/plugins/embeddable/server/plugin.ts +++ b/src/plugins/embeddable/server/plugin.ts @@ -32,23 +32,32 @@ import { getMigrateFunction, getTelemetryFunction, } from '../common/lib'; -import { SerializableState } from '../../kibana_utils/common'; +import { PersistableStateService, SerializableState } from '../../kibana_utils/common'; import { EmbeddableStateWithType } from '../common/types'; -export interface EmbeddableSetup { - getAttributeService: any; +export interface EmbeddableSetup extends PersistableStateService { registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => void; registerEnhancement: (enhancement: EnhancementRegistryDefinition) => void; } -export class EmbeddableServerPlugin implements Plugin { +export type EmbeddableStart = PersistableStateService; + +export class EmbeddableServerPlugin implements Plugin { private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map(); private readonly enhancements: EnhancementsRegistry = new Map(); public setup(core: CoreSetup) { + const commonContract = { + getEmbeddableFactory: this.getEmbeddableFactory, + getEnhancement: this.getEnhancement, + }; return { registerEmbeddableFactory: this.registerEmbeddableFactory, registerEnhancement: this.registerEnhancement, + telemetry: getTelemetryFunction(commonContract), + extract: getExtractFunction(commonContract), + inject: getInjectFunction(commonContract), + migrate: getMigrateFunction(commonContract), }; } diff --git a/src/plugins/embeddable/server/server.api.md b/src/plugins/embeddable/server/server.api.md index 87f7d76cffaa85..d3921ab11457c8 100644 --- a/src/plugins/embeddable/server/server.api.md +++ b/src/plugins/embeddable/server/server.api.md @@ -18,12 +18,11 @@ export interface EmbeddableRegistryDefinition

{ // (undocumented) registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => void; // (undocumented) diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/constants.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/constants.ts new file mode 100644 index 00000000000000..922ec36619a4b9 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/constants.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. + */ + +/** + * NOTE: DO NOT CHANGE THIS STRING WITHOUT CAREFUL CONSIDERATOIN, BECAUSE IT IS + * STORED IN SAVED OBJECTS. + * + * Also temporary dashboard drilldown migration code inside embeddable plugin relies on it + * x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts + */ +export const EMBEDDABLE_TO_DASHBOARD_DRILLDOWN = 'DASHBOARD_TO_DASHBOARD_DRILLDOWN'; diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.test.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.test.ts new file mode 100644 index 00000000000000..dd890b2463226b --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.test.ts @@ -0,0 +1,48 @@ +/* + * 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 { createExtract, createInject } from './dashboard_drilldown_persistable_state'; +import { SerializedEvent } from '../../../../ui_actions_enhanced/common'; + +const drilldownId = 'test_id'; +const extract = createExtract({ drilldownId }); +const inject = createInject({ drilldownId }); + +const state: SerializedEvent = { + eventId: 'event_id', + triggers: [], + action: { + factoryId: drilldownId, + name: 'name', + config: { + dashboardId: 'dashboardId_1', + }, + }, +}; + +test('should extract and injected dashboard reference', () => { + const { state: extractedState, references } = extract(state); + expect(extractedState).not.toEqual(state); + expect(extractedState.action.config.dashboardId).toBeUndefined(); + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "dashboardId_1", + "name": "drilldown:test_id:event_id:dashboardId", + "type": "dashboard", + }, + ] + `); + + let injectedState = inject(extractedState, references); + expect(injectedState).toEqual(state); + + references[0].id = 'dashboardId_2'; + + injectedState = inject(extractedState, references); + expect(injectedState).not.toEqual(extractedState); + expect(injectedState.action.config.dashboardId).toBe('dashboardId_2'); +}); diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.ts new file mode 100644 index 00000000000000..bd972723c649b2 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.ts @@ -0,0 +1,75 @@ +/* + * 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 { SavedObjectReference } from '../../../../../../src/core/types'; +import { PersistableStateService } from '../../../../../../src/plugins/kibana_utils/common'; +import { SerializedAction, SerializedEvent } from '../../../../ui_actions_enhanced/common'; +import { DrilldownConfig } from './types'; + +type DashboardDrilldownPersistableState = PersistableStateService; + +const generateRefName = (state: SerializedEvent, id: string) => + `drilldown:${id}:${state.eventId}:dashboardId`; + +const injectDashboardId = (state: SerializedEvent, dashboardId: string): SerializedEvent => { + return { + ...state, + action: { + ...state.action, + config: { + ...state.action.config, + dashboardId, + }, + }, + }; +}; + +export const createInject = ({ + drilldownId, +}: { + drilldownId: string; +}): DashboardDrilldownPersistableState['inject'] => { + return (state: SerializedEvent, references: SavedObjectReference[]) => { + const action = state.action as SerializedAction; + const refName = generateRefName(state, drilldownId); + const ref = references.find((r) => r.name === refName); + if (!ref) return state; + if (ref.id && ref.id === action.config.dashboardId) return state; + return injectDashboardId(state, ref.id); + }; +}; + +export const createExtract = ({ + drilldownId, +}: { + drilldownId: string; +}): DashboardDrilldownPersistableState['extract'] => { + return (state: SerializedEvent) => { + const action = state.action as SerializedAction; + const references: SavedObjectReference[] = action.config.dashboardId + ? [ + { + name: generateRefName(state, drilldownId), + type: 'dashboard', + id: action.config.dashboardId, + }, + ] + : []; + + const { dashboardId, ...restOfConfig } = action.config; + + return { + state: { + ...state, + action: ({ + ...state.action, + config: restOfConfig, + } as unknown) as SerializedAction, + }, + references, + }; + }; +}; diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/index.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/index.ts new file mode 100644 index 00000000000000..f6a757ad7a1801 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { createExtract, createInject } from './dashboard_drilldown_persistable_state'; +export { EMBEDDABLE_TO_DASHBOARD_DRILLDOWN } from './constants'; +export { DrilldownConfig } from './types'; diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/types.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/types.ts new file mode 100644 index 00000000000000..3be2a9739837ed --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/types.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type DrilldownConfig = { + dashboardId?: string; + useCurrentFilters: boolean; + useCurrentDateRange: boolean; +}; diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/index.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/index.ts new file mode 100644 index 00000000000000..76c9abbd4bfbe3 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/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 * from './dashboard_drilldown'; diff --git a/x-pack/plugins/dashboard_enhanced/common/index.ts b/x-pack/plugins/dashboard_enhanced/common/index.ts new file mode 100644 index 00000000000000..8cc3e129065311 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/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 * from './drilldowns'; diff --git a/x-pack/plugins/dashboard_enhanced/kibana.json b/x-pack/plugins/dashboard_enhanced/kibana.json index f79a69c9f4aba5..b24c0b6983f407 100644 --- a/x-pack/plugins/dashboard_enhanced/kibana.json +++ b/x-pack/plugins/dashboard_enhanced/kibana.json @@ -1,7 +1,7 @@ { "id": "dashboardEnhanced", "version": "kibana", - "server": false, + "server": true, "ui": true, "requiredPlugins": ["data", "uiActionsEnhanced", "embeddable", "dashboard", "share"], "configPath": ["xpack", "dashboardEnhanced"], diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx index b098d66619814e..451254efd96485 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx @@ -9,19 +9,19 @@ import { DataPublicPluginStart } from 'src/plugins/data/public'; import { DashboardStart } from 'src/plugins/dashboard/public'; import { reactToUiComponent } from '../../../../../../../src/plugins/kibana_react/public'; import { - TriggerId, TriggerContextMapping, + TriggerId, } from '../../../../../../../src/plugins/ui_actions/public'; import { CollectConfigContainer } from './components'; import { - UiActionsEnhancedDrilldownDefinition as Drilldown, - UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext, AdvancedUiActionsStart, + UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext, + UiActionsEnhancedDrilldownDefinition as Drilldown, } from '../../../../../ui_actions_enhanced/public'; import { txtGoToDashboard } from './i18n'; import { - StartServicesGetter, CollectConfigProps, + StartServicesGetter, } from '../../../../../../../src/plugins/kibana_utils/public'; import { KibanaURL } from '../../../../../../../src/plugins/share/public'; import { Config } from './types'; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts index 330a501a78d391..7f5137812ee324 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts @@ -6,12 +6,8 @@ import { UiActionsEnhancedBaseActionFactoryContext } from '../../../../../ui_actions_enhanced/public'; import { APPLY_FILTER_TRIGGER } from '../../../../../../../src/plugins/ui_actions/public'; +import { DrilldownConfig } from '../../../../common'; -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -export type Config = { - dashboardId?: string; - useCurrentFilters: boolean; - useCurrentDateRange: boolean; -}; +export type Config = DrilldownConfig; export type FactoryContext = UiActionsEnhancedBaseActionFactoryContext; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx index f6de2ba931c58d..5bfb175ea0d00c 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx @@ -65,6 +65,12 @@ test('getHref is defined', () => { expect(drilldown.getHref).toBeDefined(); }); +test('inject/extract are defined', () => { + const drilldown = new EmbeddableToDashboardDrilldown({} as any); + expect(drilldown.extract).toBeDefined(); + expect(drilldown.inject).toBeDefined(); +}); + describe('.execute() & getHref', () => { /** * A convenience test setup helper diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx index 25bc93ad38b368..921c2aed00624a 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx @@ -22,6 +22,7 @@ import { } from '../abstract_dashboard_drilldown'; import { KibanaURL } from '../../../../../../../src/plugins/share/public'; import { EMBEDDABLE_TO_DASHBOARD_DRILLDOWN } from './constants'; +import { createExtract, createInject } from '../../../../common'; type Trigger = typeof APPLY_FILTER_TRIGGER; type Context = TriggerContextMapping[Trigger]; @@ -80,4 +81,8 @@ export class EmbeddableToDashboardDrilldown extends AbstractDashboardDrilldown { + constructor(protected readonly context: PluginInitializerContext) {} + + public setup(core: CoreSetup, plugins: SetupDependencies): SetupContract { + plugins.uiActionsEnhanced.registerActionFactory({ + id: EMBEDDABLE_TO_DASHBOARD_DRILLDOWN, + inject: createInject({ drilldownId: EMBEDDABLE_TO_DASHBOARD_DRILLDOWN }), + extract: createExtract({ drilldownId: EMBEDDABLE_TO_DASHBOARD_DRILLDOWN }), + }); + + return {}; + } + + public start(core: CoreStart, plugins: StartDependencies): StartContract { + return {}; + } + + public stop() {} +} diff --git a/x-pack/plugins/ui_actions_enhanced/common/index.ts b/x-pack/plugins/ui_actions_enhanced/common/index.ts new file mode 100644 index 00000000000000..9f4141dbcae7df --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/common/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 * from './types'; diff --git a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts index b3664362009149..ade78c31211ab2 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts @@ -7,11 +7,11 @@ import { EnhancementRegistryDefinition } from '../../../../src/plugins/embeddable/server'; import { SavedObjectReference } from '../../../../src/core/types'; import { DynamicActionsState, SerializedEvent } from './types'; -import { AdvancedUiActionsPublicPlugin } from './plugin'; +import { AdvancedUiActionsServerPlugin } from './plugin'; import { SerializableState } from '../../../../src/plugins/kibana_utils/common'; export const dynamicActionEnhancement = ( - uiActionsEnhanced: AdvancedUiActionsPublicPlugin + uiActionsEnhanced: AdvancedUiActionsServerPlugin ): EnhancementRegistryDefinition => { return { id: 'dynamicActions', diff --git a/x-pack/plugins/ui_actions_enhanced/server/index.ts b/x-pack/plugins/ui_actions_enhanced/server/index.ts index 5419c4135796df..e1363be35e2e9a 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/index.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/index.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AdvancedUiActionsPublicPlugin } from './plugin'; +import { AdvancedUiActionsServerPlugin } from './plugin'; export function plugin() { - return new AdvancedUiActionsPublicPlugin(); + return new AdvancedUiActionsServerPlugin(); } -export { AdvancedUiActionsPublicPlugin as Plugin }; +export { AdvancedUiActionsServerPlugin as Plugin }; export { SetupContract as AdvancedUiActionsSetup, StartContract as AdvancedUiActionsStart, diff --git a/x-pack/plugins/ui_actions_enhanced/server/plugin.ts b/x-pack/plugins/ui_actions_enhanced/server/plugin.ts index d6d18848be4de4..718304018730de 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/plugin.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/plugin.ts @@ -16,7 +16,7 @@ import { } from './types'; export interface SetupContract { - registerActionFactory: any; + registerActionFactory: (definition: ActionFactoryDefinition) => void; } export type StartContract = void; @@ -25,7 +25,7 @@ interface SetupDependencies { embeddable: EmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. } -export class AdvancedUiActionsPublicPlugin +export class AdvancedUiActionsServerPlugin implements Plugin { protected readonly actionFactories: ActionFactoryRegistry = new Map(); diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts index 43b88915b69d90..9326f7e240e3e4 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts @@ -14,7 +14,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardPanelActions = getService('dashboardPanelActions'); const dashboardDrilldownPanelActions = getService('dashboardDrilldownPanelActions'); const dashboardDrilldownsManage = getService('dashboardDrilldownsManage'); - const PageObjects = getPageObjects(['dashboard', 'common', 'header', 'timePicker']); + const PageObjects = getPageObjects([ + 'dashboard', + 'common', + 'header', + 'timePicker', + 'settings', + 'copySavedObjectsToSpace', + ]); const pieChart = getService('pieChart'); const log = getService('log'); const browser = getService('browser'); @@ -22,120 +29,188 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const filterBar = getService('filterBar'); const security = getService('security'); + const spaces = getService('spaces'); describe('Dashboard to dashboard drilldown', function () { - before(async () => { - log.debug('Dashboard Drilldowns:initTests'); - await security.testUser.setRoles(['test_logstash_reader', 'global_dashboard_all']); - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.preserveCrossAppState(); - }); - - after(async () => { - await security.testUser.restoreDefaults(); - }); - - it('should create dashboard to dashboard drilldown, use it, and then delete it', async () => { - await PageObjects.dashboard.gotoDashboardEditMode( - dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME - ); - - // create drilldown - await dashboardPanelActions.openContextMenu(); - await dashboardDrilldownPanelActions.expectExistsCreateDrilldownAction(); - await dashboardDrilldownPanelActions.clickCreateDrilldown(); - await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutOpen(); - await dashboardDrilldownsManage.fillInDashboardToDashboardDrilldownWizard({ - drilldownName: DRILLDOWN_TO_AREA_CHART_NAME, - destinationDashboardTitle: dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME, + describe('Create & use drilldowns', () => { + before(async () => { + log.debug('Dashboard Drilldowns:initTests'); + await security.testUser.setRoles(['test_logstash_reader', 'global_dashboard_all']); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); }); - await dashboardDrilldownsManage.saveChanges(); - await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutClose(); - - // check that drilldown notification badge is shown - expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(1); - - // save dashboard, navigate to view mode - await PageObjects.dashboard.saveDashboard( - dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME, - { - saveAsNew: false, - waitDialogIsClosed: true, - } - ); - - // trigger drilldown action by clicking on a pie and picking drilldown action by it's name - await pieChart.clickOnPieSlice('40,000'); - await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); - - const href = await dashboardDrilldownPanelActions.getActionHrefByText( - DRILLDOWN_TO_AREA_CHART_NAME - ); - expect(typeof href).to.be('string'); // checking that action has a href - const dashboardIdFromHref = PageObjects.dashboard.getDashboardIdFromUrl(href); - - await navigateWithinDashboard(async () => { - await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_AREA_CHART_NAME); - }); - // checking that href is at least pointing to the same dashboard that we are navigated to by regular click - expect(dashboardIdFromHref).to.be(await PageObjects.dashboard.getDashboardIdFromCurrentUrl()); - - // check that we drilled-down with filter from pie chart - expect(await filterBar.getFilterCount()).to.be(1); - - const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - // brush area chart and drilldown back to pie chat dashboard - await brushAreaChart(); - await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); - - await navigateWithinDashboard(async () => { - await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + after(async () => { + await security.testUser.restoreDefaults(); }); - // because filters are preserved during navigation, we expect that only one slice is displayed (filter is still applied) - expect(await filterBar.getFilterCount()).to.be(1); - await pieChart.expectPieSliceCount(1); - - // check that new time range duration was applied - const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); - - // delete drilldown - await PageObjects.dashboard.switchToEditMode(); - await dashboardPanelActions.openContextMenu(); - await dashboardDrilldownPanelActions.expectExistsManageDrilldownsAction(); - await dashboardDrilldownPanelActions.clickManageDrilldowns(); - await dashboardDrilldownsManage.expectsManageDrilldownsFlyoutOpen(); - - await dashboardDrilldownsManage.deleteDrilldownsByTitles([DRILLDOWN_TO_AREA_CHART_NAME]); - await dashboardDrilldownsManage.closeFlyout(); + it('should create dashboard to dashboard drilldown, use it, and then delete it', async () => { + await PageObjects.dashboard.gotoDashboardEditMode( + dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME + ); + + // create drilldown + await dashboardPanelActions.openContextMenu(); + await dashboardDrilldownPanelActions.expectExistsCreateDrilldownAction(); + await dashboardDrilldownPanelActions.clickCreateDrilldown(); + await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutOpen(); + await dashboardDrilldownsManage.fillInDashboardToDashboardDrilldownWizard({ + drilldownName: DRILLDOWN_TO_AREA_CHART_NAME, + destinationDashboardTitle: dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME, + }); + await dashboardDrilldownsManage.saveChanges(); + await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutClose(); + + // check that drilldown notification badge is shown + expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(1); + + // save dashboard, navigate to view mode + await PageObjects.dashboard.saveDashboard( + dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME, + { + saveAsNew: false, + waitDialogIsClosed: true, + } + ); + + // trigger drilldown action by clicking on a pie and picking drilldown action by it's name + await pieChart.clickOnPieSlice('40,000'); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + + const href = await dashboardDrilldownPanelActions.getActionHrefByText( + DRILLDOWN_TO_AREA_CHART_NAME + ); + expect(typeof href).to.be('string'); // checking that action has a href + const dashboardIdFromHref = PageObjects.dashboard.getDashboardIdFromUrl(href); + + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_AREA_CHART_NAME); + }); + // checking that href is at least pointing to the same dashboard that we are navigated to by regular click + expect(dashboardIdFromHref).to.be( + await PageObjects.dashboard.getDashboardIdFromCurrentUrl() + ); + + // check that we drilled-down with filter from pie chart + expect(await filterBar.getFilterCount()).to.be(1); + + const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + + // brush area chart and drilldown back to pie chat dashboard + await brushAreaChart(); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + }); + + // because filters are preserved during navigation, we expect that only one slice is displayed (filter is still applied) + expect(await filterBar.getFilterCount()).to.be(1); + await pieChart.expectPieSliceCount(1); + + // check that new time range duration was applied + const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); + + // delete drilldown + await PageObjects.dashboard.switchToEditMode(); + await dashboardPanelActions.openContextMenu(); + await dashboardDrilldownPanelActions.expectExistsManageDrilldownsAction(); + await dashboardDrilldownPanelActions.clickManageDrilldowns(); + await dashboardDrilldownsManage.expectsManageDrilldownsFlyoutOpen(); + + await dashboardDrilldownsManage.deleteDrilldownsByTitles([DRILLDOWN_TO_AREA_CHART_NAME]); + await dashboardDrilldownsManage.closeFlyout(); + + // check that drilldown notification badge is shown + expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(0); + }); - // check that drilldown notification badge is shown - expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(0); + it('browser back/forward navigation works after drilldown navigation', async () => { + await PageObjects.dashboard.loadSavedDashboard( + dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME + ); + const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + await brushAreaChart(); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + }); + // check that new time range duration was applied + const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); + + await navigateWithinDashboard(async () => { + await browser.goBack(); + }); + + expect(await PageObjects.timePicker.getTimeDurationInHours()).to.be( + originalTimeRangeDurationHours + ); + }); }); - it('browser back/forward navigation works after drilldown navigation', async () => { - await PageObjects.dashboard.loadSavedDashboard( - dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME - ); - const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - await brushAreaChart(); - await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); - await navigateWithinDashboard(async () => { - await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + describe('Copy to space', () => { + const destinationSpaceId = 'custom_space'; + before(async () => { + await spaces.create({ + id: destinationSpaceId, + name: 'custom_space', + disabledFeatures: [], + }); + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSavedObjects(); }); - // check that new time range duration was applied - const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); - await navigateWithinDashboard(async () => { - await browser.goBack(); + after(async () => { + await spaces.delete(destinationSpaceId); }); - expect(await PageObjects.timePicker.getTimeDurationInHours()).to.be( - originalTimeRangeDurationHours - ); + it('Dashboards linked by a drilldown are both copied to a space', async () => { + await PageObjects.copySavedObjectsToSpace.openCopyToSpaceFlyoutForObject( + dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME + ); + await PageObjects.copySavedObjectsToSpace.setupForm({ + destinationSpaceId, + }); + await PageObjects.copySavedObjectsToSpace.startCopy(); + + // Wait for successful copy + await testSubjects.waitForDeleted(`cts-summary-indicator-loading-${destinationSpaceId}`); + await testSubjects.existOrFail(`cts-summary-indicator-success-${destinationSpaceId}`); + + const summaryCounts = await PageObjects.copySavedObjectsToSpace.getSummaryCounts(); + + expect(summaryCounts).to.eql({ + success: 5, // 2 dashboards (linked by a drilldown) + 2 visualizations + 1 index pattern + pending: 0, + skipped: 0, + errors: 0, + }); + + await PageObjects.copySavedObjectsToSpace.finishCopy(); + + // Actually use copied dashboards in a new space: + + await PageObjects.common.navigateToApp('dashboard', { + basePath: `/s/${destinationSpaceId}`, + }); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.loadSavedDashboard( + dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME + ); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); + + // brush area chart and drilldown back to pie chat dashboard + await brushAreaChart(); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + }); + await pieChart.expectPieSliceCount(10); + }); }); }); diff --git a/x-pack/test/functional/es_archives/reporting/hugedata/data.json.gz b/x-pack/test/functional/es_archives/reporting/hugedata/data.json.gz index c524379640df7e2c11cd0f86603cf4048a826bd3..c616730ff35b64074b036f0bf30b77d0e45b6c75 100644 GIT binary patch delta 32038 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 NJ4CM=j|g~({|Afl)R_PP diff --git a/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json b/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json index 3434e1f80a7cee..552142d3b190ae 100644 --- a/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json +++ b/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json @@ -126,7 +126,7 @@ "title": "Dashboard Foo", "hits": 0, "description": "", - "panelsJSON": "[{}]", + "panelsJSON": "[]", "optionsJSON": "{}", "version": 1, "timeRestore": false, @@ -156,7 +156,7 @@ "title": "Dashboard Bar", "hits": 0, "description": "", - "panelsJSON": "[{}]", + "panelsJSON": "[]", "optionsJSON": "{}", "version": 1, "timeRestore": false, diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json index ef08d693242104..7f416c26cc9aae 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json @@ -11,6 +11,12 @@ "title": "[Logs Sample] Overview ECS", "version": 1 }, + "references": [ + { "id": "sample_visualization", "name": "panel_0", "type": "visualization" }, + { "id": "sample_search", "name": "panel_1", "type": "search" }, + { "id": "sample_search", "name": "panel_2", "type": "search" }, + { "id": "sample_visualization", "name": "panel_3", "type": "visualization" } + ], "id": "sample_dashboard", "type": "dashboard" -} \ No newline at end of file +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard2.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard2.json index 7ea63c5d444ba9..c99506fec3cf54 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard2.json +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard2.json @@ -11,6 +11,12 @@ "title": "[Logs Sample2] Overview ECS", "version": 1 }, + "references": [ + { "id": "sample_visualization", "name": "panel_0", "type": "visualization" }, + { "id": "sample_search", "name": "panel_1", "type": "search" }, + { "id": "sample_search", "name": "panel_2", "type": "search" }, + { "id": "sample_visualization", "name": "panel_3", "type": "visualization" } + ], "id": "sample_dashboard2", "type": "dashboard" -} \ No newline at end of file +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json index ef08d693242104..4513c07f277867 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json @@ -11,6 +11,12 @@ "title": "[Logs Sample] Overview ECS", "version": 1 }, + "references": [ + { "id": "sample_visualization", "name": "panel_0", "type": "visualization" }, + { "id": "sample_search2", "name": "panel_1", "type": "search" }, + { "id": "sample_search2", "name": "panel_2", "type": "search" }, + { "id": "sample_visualization", "name": "panel_3", "type": "visualization" } + ], "id": "sample_dashboard", "type": "dashboard" -} \ No newline at end of file +} From bd99b19bd81a635da4a81de2cfd7f36ccca24014 Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 12 Nov 2020 07:42:41 -0800 Subject: [PATCH 25/52] [App Search] Version documentation links (#83245) * Fix CURRENT_MAJOR_VERSION for use in Elastic docs links - Was previously just sending (e.g.) "7". instead of "7.9" * Add App Search DOCS_PREFIX constant - follow WS's example * Update all App Search doc links to use prefixed URLs - except for Enterprise Search setup guide, which should be updated to use a shared URL at some point in any case --- x-pack/plugins/enterprise_search/common/version.ts | 2 +- .../app_search/components/credentials/constants.ts | 3 ++- .../settings/log_retention/log_retention_panel.tsx | 7 +++---- .../app_search/components/setup_guide/setup_guide.tsx | 5 +++-- .../public/applications/app_search/routes.ts | 4 ++++ 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/enterprise_search/common/version.ts b/x-pack/plugins/enterprise_search/common/version.ts index e29ad8a9f866b9..c23b05f7cdb3d8 100644 --- a/x-pack/plugins/enterprise_search/common/version.ts +++ b/x-pack/plugins/enterprise_search/common/version.ts @@ -8,4 +8,4 @@ import { SemVer } from 'semver'; import pkg from '../../../../package.json'; export const CURRENT_VERSION = new SemVer(pkg.version as string); -export const CURRENT_MAJOR_VERSION = CURRENT_VERSION.major; +export const CURRENT_MAJOR_VERSION = `${CURRENT_VERSION.major}.${CURRENT_VERSION.minor}`; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts index ea4906ec08946b..2b96e3322cd555 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { DOCS_PREFIX } from '../../routes'; export const CREDENTIALS_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.credentials.title', @@ -100,4 +101,4 @@ export const TOKEN_TYPE_INFO = [ export const FLYOUT_ARIA_LABEL_ID = 'credentialsFlyoutTitle'; -export const DOCS_HREF = 'https://www.elastic.co/guide/en/app-search/current/authentication.html'; +export const DOCS_HREF = `${DOCS_PREFIX}/authentication.html`; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx index 23572074b3c692..3297f0df4d7bd2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx @@ -10,6 +10,8 @@ import { i18n } from '@kbn/i18n'; import { EuiLink, EuiSpacer, EuiSwitch, EuiText, EuiTextColor, EuiTitle } from '@elastic/eui'; import { useActions, useValues } from 'kea'; +import { DOCS_PREFIX } from '../../../routes'; + import { LogRetentionLogic } from './log_retention_logic'; import { AnalyticsLogRetentionMessage, ApiLogRetentionMessage } from './messaging'; import { LogRetentionOptions } from './types'; @@ -41,10 +43,7 @@ export const LogRetentionPanel: React.FC = () => { {i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.description', { defaultMessage: 'Manage the default write settings for API Logs and Analytics.', })}{' '} - + {i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.learnMore', { defaultMessage: 'Learn more about retention settings.', })} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx index 60d7f6951a478c..b3faa73dfaed6a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx @@ -13,14 +13,15 @@ import { APP_SEARCH_PLUGIN } from '../../../../../common/constants'; import { SetupGuide as SetupGuideLayout } from '../../../shared/setup_guide'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; +import { DOCS_PREFIX } from '../../routes'; import GettingStarted from './assets/getting_started.png'; export const SetupGuide: React.FC = () => ( Date: Thu, 12 Nov 2020 16:49:47 +0100 Subject: [PATCH 26/52] [Lens] Add suffix formatter (#82852) --- .../indexpattern_datasource/format_column.ts | 16 +++++- .../public/indexpattern_datasource/index.ts | 2 + .../indexpattern_datasource/indexpattern.tsx | 1 + .../suffix_formatter.test.ts | 28 ++++++++++ .../suffix_formatter.ts | 51 +++++++++++++++++++ .../indexpattern_datasource/time_scale.ts | 2 +- 6 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.test.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.ts diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/format_column.ts b/x-pack/plugins/lens/public/indexpattern_datasource/format_column.ts index 3666528f431667..1f337298a03adb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/format_column.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/format_column.ts @@ -116,9 +116,18 @@ export const formatColumn: ExpressionFunctionDefinition< }); } if (parentFormatParams) { - const innerParams = (col.meta.params?.params as Record) ?? {}; + // if original format is already a nested one, we are just replacing the wrapper params + // otherwise wrapping it inside parentFormatId/parentFormatParams + const isNested = isNestedFormat(col.meta.params); + const innerParams = isNested + ? col.meta.params?.params + : { id: col.meta.params?.id, params: col.meta.params?.params }; + + const formatId = isNested ? col.meta.params?.id : parentFormatId; + return withParams(col, { ...col.meta.params, + id: formatId, params: { ...innerParams, ...parentFormatParams, @@ -132,6 +141,11 @@ export const formatColumn: ExpressionFunctionDefinition< }, }; +function isNestedFormat(params: DatatableColumn['meta']['params']) { + // if there is a nested params object with an id, it's a nested format + return !!params?.params?.id; +} + function withParams(col: DatatableColumn, params: Record) { return { ...col, meta: { ...col.meta, params } }; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts index 92280b0fb6ce6f..793f3387e707d3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts @@ -38,8 +38,10 @@ export class IndexPatternDatasource { renameColumns, formatColumn, getTimeScaleFunction, + getSuffixFormatter, } = await import('../async_services'); return core.getStartServices().then(([coreStart, { data }]) => { + data.fieldFormats.register([getSuffixFormatter(data.fieldFormats.deserialize)]); expressions.registerFunction(getTimeScaleFunction(data)); expressions.registerFunction(renameColumns); expressions.registerFunction(formatColumn); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index e37c31559cd0ca..94f240058d6189 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -78,6 +78,7 @@ export function columnToOperation(column: IndexPatternColumn, uniqueLabel?: stri export * from './rename_columns'; export * from './format_column'; export * from './time_scale'; +export * from './suffix_formatter'; export function getIndexPatternDatasource({ core, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.test.ts new file mode 100644 index 00000000000000..ef1739e4424fa3 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.test.ts @@ -0,0 +1,28 @@ +/* + * 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 { FormatFactory } from '../types'; +import { getSuffixFormatter } from './suffix_formatter'; + +describe('suffix formatter', () => { + it('should call nested formatter and apply suffix', () => { + const convertMock = jest.fn((x) => x); + const formatFactory = jest.fn(() => ({ convert: convertMock })); + const SuffixFormatter = getSuffixFormatter((formatFactory as unknown) as FormatFactory); + const nestedParams = { abc: 123 }; + const formatterInstance = new SuffixFormatter({ + unit: 'h', + id: 'nestedFormatter', + params: nestedParams, + }); + + const result = formatterInstance.convert(12345); + + expect(result).toEqual('12345/h'); + expect(convertMock).toHaveBeenCalledWith(12345); + expect(formatFactory).toHaveBeenCalledWith({ id: 'nestedFormatter', params: nestedParams }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.ts b/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.ts new file mode 100644 index 00000000000000..5594976738efee --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { FieldFormat, KBN_FIELD_TYPES } from '../../../../../src/plugins/data/public'; +import { FormatFactory } from '../types'; +import { TimeScaleUnit } from './time_scale'; + +const unitSuffixes: Record = { + s: i18n.translate('xpack.lens.fieldFormats.suffix.s', { defaultMessage: '/h' }), + m: i18n.translate('xpack.lens.fieldFormats.suffix.m', { defaultMessage: '/m' }), + h: i18n.translate('xpack.lens.fieldFormats.suffix.h', { defaultMessage: '/h' }), + d: i18n.translate('xpack.lens.fieldFormats.suffix.d', { defaultMessage: '/d' }), +}; + +export function getSuffixFormatter(formatFactory: FormatFactory) { + return class SuffixFormatter extends FieldFormat { + static id = 'suffix'; + static title = i18n.translate('xpack.lens.fieldFormats.suffix.title', { + defaultMessage: 'Suffix', + }); + static fieldType = KBN_FIELD_TYPES.NUMBER; + allowsNumericalAggregations = true; + + getParamDefaults() { + return { + unit: undefined, + nestedParams: {}, + }; + } + + textConvert = (val: unknown) => { + const unit = this.param('unit') as TimeScaleUnit | undefined; + const suffix = unit ? unitSuffixes[unit] : undefined; + const nestedFormatter = this.param('id'); + const nestedParams = this.param('params'); + + const formattedValue = formatFactory({ id: nestedFormatter, params: nestedParams }).convert( + val + ); + + if (suffix) { + return `${formattedValue}${suffix}`; + } + return formattedValue; + }; + }; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts b/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts index 7a4e8f6bc0638a..06ff8058b1d09e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts @@ -11,7 +11,7 @@ import { DataPublicPluginStart } from 'src/plugins/data/public'; import { search } from '../../../../../src/plugins/data/public'; import { buildResultColumns } from '../../../../../src/plugins/expressions/common'; -type TimeScaleUnit = 's' | 'm' | 'h' | 'd'; +export type TimeScaleUnit = 's' | 'm' | 'h' | 'd'; export interface TimeScaleArgs { dateColumnId: string; From 4932dc55a6f4e97690a5b2d659eb0a854d65cf17 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Thu, 12 Nov 2020 08:58:05 -0700 Subject: [PATCH 27/52] [Reporting] Move "common" types and constants to allow cross-plugin integration (#83198) --- x-pack/plugins/reporting/common/constants.ts | 41 +++++- x-pack/plugins/reporting/common/index.ts | 9 ++ x-pack/plugins/reporting/common/poller.ts | 11 +- x-pack/plugins/reporting/common/types.ts | 127 +++++++++++++++--- x-pack/plugins/reporting/constants.ts | 39 ------ .../buttons/report_download_button.tsx | 4 +- .../buttons/report_error_button.tsx | 4 +- .../components/buttons/report_info_button.tsx | 2 +- .../reporting/public/components/index.ts | 1 + .../public/components/job_download_button.tsx | 3 +- .../public/components/job_failure.tsx | 2 +- .../public/components/job_success.tsx | 3 +- .../components/job_warning_formulas.tsx | 3 +- .../components/job_warning_max_size.tsx | 3 +- .../public/components/report_listing.tsx | 2 +- .../components/reporting_panel_content.tsx | 7 +- x-pack/plugins/reporting/public/index.ts | 34 ++--- .../lib/job_completion_notifications.ts | 2 +- .../public/lib/reporting_api_client.ts | 12 +- .../public/lib/stream_handler.test.ts | 3 +- .../reporting/public/lib/stream_handler.ts | 14 +- .../panel_actions/get_csv_panel_action.tsx | 16 +-- x-pack/plugins/reporting/public/plugin.ts | 32 ++++- .../server/export_types/csv/create_job.ts | 2 +- .../server/export_types/csv/index.ts | 2 +- .../csv_from_savedobject/index.ts | 2 +- .../csv_from_savedobject/metadata.ts | 2 +- .../export_types/png/create_job/index.ts | 2 +- .../printable_pdf/create_job/index.ts | 2 +- .../server/lib/layouts/create_layout.ts | 5 +- .../reporting/server/lib/layouts/index.ts | 54 ++------ .../server/lib/layouts/preserve_layout.ts | 15 +-- .../server/lib/layouts/print_layout.ts | 14 +- .../reporting/server/lib/store/index.ts | 3 +- .../reporting/server/lib/store/report.ts | 51 +------ .../reporting/server/lib/tasks/index.ts | 11 +- .../generate_from_savedobject_immediate.ts | 4 +- .../create_mock_layoutinstance.ts | 5 +- x-pack/plugins/reporting/server/types.ts | 13 +- 39 files changed, 282 insertions(+), 279 deletions(-) delete mode 100644 x-pack/plugins/reporting/constants.ts diff --git a/x-pack/plugins/reporting/common/constants.ts b/x-pack/plugins/reporting/common/constants.ts index 07a239494da239..16e40bab65a46e 100644 --- a/x-pack/plugins/reporting/common/constants.ts +++ b/x-pack/plugins/reporting/common/constants.ts @@ -11,13 +11,6 @@ export const BROWSER_TYPE = 'chromium'; export const JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY = 'xpack.reporting.jobCompletionNotifications'; -export const API_BASE_URL = '/api/reporting'; // "Generation URL" from share menu -export const API_BASE_URL_V1 = '/api/reporting/v1'; // -export const API_BASE_GENERATE_V1 = `${API_BASE_URL_V1}/generate`; -export const API_LIST_URL = '/api/reporting/jobs'; -export const API_GENERATE_IMMEDIATE = `${API_BASE_URL_V1}/generate/immediate/csv/saved-object`; -export const API_DIAGNOSE_URL = `${API_BASE_URL}/diagnose`; - export const CONTENT_TYPE_CSV = 'text/csv'; export const CSV_REPORTING_ACTION = 'downloadCsvReport'; export const CSV_BOM_CHARS = '\ufeff'; @@ -57,15 +50,49 @@ export const UI_SETTINGS_CUSTOM_PDF_LOGO = 'xpackReporting:customPdfLogo'; export const UI_SETTINGS_CSV_SEPARATOR = 'csv:separator'; export const UI_SETTINGS_CSV_QUOTE_VALUES = 'csv:quoteValues'; +export const LAYOUT_TYPES = { + PRESERVE_LAYOUT: 'preserve_layout', + PRINT: 'print', +}; + +// Export Type Definitions +export const CSV_REPORT_TYPE = 'CSV'; +export const PDF_REPORT_TYPE = 'printablePdf'; +export const PNG_REPORT_TYPE = 'PNG'; + export const PDF_JOB_TYPE = 'printable_pdf'; export const PNG_JOB_TYPE = 'PNG'; export const CSV_JOB_TYPE = 'csv'; export const CSV_FROM_SAVEDOBJECT_JOB_TYPE = 'csv_from_savedobject'; export const USES_HEADLESS_JOB_TYPES = [PDF_JOB_TYPE, PNG_JOB_TYPE]; +// Licenses export const LICENSE_TYPE_TRIAL = 'trial'; export const LICENSE_TYPE_BASIC = 'basic'; export const LICENSE_TYPE_STANDARD = 'standard'; export const LICENSE_TYPE_GOLD = 'gold'; export const LICENSE_TYPE_PLATINUM = 'platinum'; export const LICENSE_TYPE_ENTERPRISE = 'enterprise'; + +// Routes +export const API_BASE_URL = '/api/reporting'; // "Generation URL" from share menu +export const API_BASE_GENERATE = `${API_BASE_URL}/generate`; +export const API_LIST_URL = `${API_BASE_URL}/jobs`; +export const API_DIAGNOSE_URL = `${API_BASE_URL}/diagnose`; + +// hacky endpoint +export const API_BASE_URL_V1 = '/api/reporting/v1'; // +export const API_GENERATE_IMMEDIATE = `${API_BASE_URL_V1}/generate/immediate/csv/saved-object`; + +// Management UI route +export const REPORTING_MANAGEMENT_HOME = '/app/management/insightsAndAlerting/reporting'; + +// Statuses +export enum JOB_STATUSES { + PENDING = 'pending', + PROCESSING = 'processing', + COMPLETED = 'completed', + FAILED = 'failed', + CANCELLED = 'cancelled', + WARNINGS = 'completed_with_warnings', +} diff --git a/x-pack/plugins/reporting/common/index.ts b/x-pack/plugins/reporting/common/index.ts index cda8934fc8bf62..0be6ab66827746 100644 --- a/x-pack/plugins/reporting/common/index.ts +++ b/x-pack/plugins/reporting/common/index.ts @@ -4,5 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { LayoutSelectorDictionary } from './types'; + export { CancellationToken } from './cancellation_token'; export { Poller } from './poller'; + +export const getDefaultLayoutSelectors = (): LayoutSelectorDictionary => ({ + screenshot: '[data-shared-items-container]', + renderComplete: '[data-shared-item]', + itemsCountAttribute: 'data-shared-items-count', + timefilterDurationAttribute: 'data-shared-timefilter-duration', +}); diff --git a/x-pack/plugins/reporting/common/poller.ts b/x-pack/plugins/reporting/common/poller.ts index 2127a876f4a271..017dbac13e29b5 100644 --- a/x-pack/plugins/reporting/common/poller.ts +++ b/x-pack/plugins/reporting/common/poller.ts @@ -5,7 +5,16 @@ */ import _ from 'lodash'; -import { PollerOptions } from './types'; + +interface PollerOptions { + functionToPoll: () => Promise; + pollFrequencyInMillis: number; + trailing?: boolean; + continuePollingOnError?: boolean; + pollFrequencyErrorMultiplier?: number; + successFunction?: (...args: any) => any; + errorFunction?: (error: Error) => any; +} // @TODO Maybe move to observables someday export class Poller { diff --git a/x-pack/plugins/reporting/common/types.ts b/x-pack/plugins/reporting/common/types.ts index 24c126bfe0571f..abd0bee7fb6ea2 100644 --- a/x-pack/plugins/reporting/common/types.ts +++ b/x-pack/plugins/reporting/common/types.ts @@ -4,15 +4,94 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { ReportingConfigType } from '../server/config'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutParams } from '../server/lib/layouts'; -export { LayoutParams }; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { ReportDocument, ReportSource } from '../server/lib/store/report'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { BaseParams } from '../server/types'; +export interface PageSizeParams { + pageMarginTop: number; + pageMarginBottom: number; + pageMarginWidth: number; + tableBorderWidth: number; + headingHeight: number; + subheadingHeight: number; +} + +export interface LayoutSelectorDictionary { + screenshot: string; + renderComplete: string; + itemsCountAttribute: string; + timefilterDurationAttribute: string; +} + +export interface PdfImageSize { + width: number; + height?: number; +} + +export interface Size { + width: number; + height: number; +} + +export interface LayoutParams { + id: string; + dimensions?: Size; + selectors?: LayoutSelectorDictionary; +} + +export interface ReportDocumentHead { + _id: string; + _index: string; + _seq_no: unknown; + _primary_term: unknown; +} + +export interface TaskRunResult { + content_type: string | null; + content: string | null; + csv_contains_formulas?: boolean; + size: number; + max_size_reached?: boolean; + warnings?: string[]; +} + +export interface ReportSource { + jobtype: string; + kibana_name: string; + kibana_id: string; + created_by: string | false; + payload: { + headers: string; // encrypted headers + browserTimezone?: string; // may use timezone from advanced settings + objectType: string; + title: string; + layout?: LayoutParams; + }; + meta: { objectType: string; layout?: string }; + browser_type: string; + max_attempts: number; + timeout: number; + + status: JobStatus; + attempts: number; + output: TaskRunResult | null; + started_at?: string; + completed_at?: string; + created_at: string; + priority?: number; + process_expiration?: string; +} + +/* + * The document created by Reporting to store in the .reporting index + */ +export interface ReportDocument extends ReportDocumentHead { + _source: ReportSource; +} + +export interface BaseParams { + browserTimezone?: string; // browserTimezone is optional: it is not in old POST URLs that were generated prior to being added to this interface + layout?: LayoutParams; + objectType: string; + title: string; +} export type JobId = string; export type JobStatus = @@ -59,18 +138,28 @@ export interface ReportApiJSON { status: string; } -export interface PollerOptions { - functionToPoll: () => Promise; - pollFrequencyInMillis: number; - trailing?: boolean; - continuePollingOnError?: boolean; - pollFrequencyErrorMultiplier?: number; - successFunction?: (...args: any) => any; - errorFunction?: (error: Error) => any; -} - export interface LicenseCheckResults { enableLinks: boolean; showLinks: boolean; message: string; } + +export interface JobSummary { + id: JobId; + status: JobStatus; + title: string; + jobtype: string; + maxSizeReached?: boolean; + csvContainsFormulas?: boolean; +} + +export interface JobSummarySet { + completed: JobSummary[]; + failed: JobSummary[]; +} + +type DownloadLink = string; +export type DownloadReportFn = (jobId: JobId) => DownloadLink; + +type ManagementLink = string; +export type ManagementLinkFn = () => ManagementLink; diff --git a/x-pack/plugins/reporting/constants.ts b/x-pack/plugins/reporting/constants.ts deleted file mode 100644 index 772c52dde4a152..00000000000000 --- a/x-pack/plugins/reporting/constants.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY = - 'xpack.reporting.jobCompletionNotifications'; - -// Routes -export const API_BASE_URL = '/api/reporting'; -export const API_LIST_URL = `${API_BASE_URL}/jobs`; -export const API_BASE_GENERATE = `${API_BASE_URL}/generate`; -export const API_GENERATE_IMMEDIATE = `${API_BASE_URL}/v1/generate/immediate/csv/saved-object`; -export const REPORTING_MANAGEMENT_HOME = '/app/management/insightsAndAlerting/reporting'; - -// Statuses -export const JOB_STATUS_FAILED = 'failed'; -export const JOB_STATUS_COMPLETED = 'completed'; -export const JOB_STATUS_WARNINGS = 'completed_with_warnings'; - -export enum JobStatuses { - PENDING = 'pending', - PROCESSING = 'processing', - COMPLETED = 'completed', - FAILED = 'failed', - CANCELLED = 'cancelled', - WARNINGS = 'completed_with_warnings', -} - -// Types -export const PDF_JOB_TYPE = 'printable_pdf'; -export const PNG_JOB_TYPE = 'PNG'; -export const CSV_JOB_TYPE = 'csv'; -export const CSV_FROM_SAVEDOBJECT_JOB_TYPE = 'csv_from_savedobject'; -export const USES_HEADLESS_JOB_TYPES = [PDF_JOB_TYPE, PNG_JOB_TYPE]; - -// Actions -export const CSV_REPORTING_ACTION = 'downloadCsvReport'; diff --git a/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx index 6c13264ebcb1fc..4bd86d15949e85 100644 --- a/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx @@ -6,7 +6,7 @@ import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import React, { FunctionComponent } from 'react'; -import { JobStatuses } from '../../../constants'; +import { JOB_STATUSES } from '../../../common/constants'; import { Job as ListingJob, Props as ListingProps } from '../report_listing'; type Props = { record: ListingJob } & ListingProps; @@ -14,7 +14,7 @@ type Props = { record: ListingJob } & ListingProps; export const ReportDownloadButton: FunctionComponent = (props: Props) => { const { record, apiClient, intl } = props; - if (record.status !== JobStatuses.COMPLETED && record.status !== JobStatuses.WARNINGS) { + if (record.status !== JOB_STATUSES.COMPLETED && record.status !== JOB_STATUSES.WARNINGS) { return null; } diff --git a/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx index 4eee86cd79ce72..2864802f843f45 100644 --- a/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx @@ -7,7 +7,7 @@ import { EuiButtonIcon, EuiCallOut, EuiPopover } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { JobStatuses } from '../../../constants'; +import { JOB_STATUSES } from '../../../common/constants'; import { JobContent, ReportingAPIClient } from '../../lib/reporting_api_client'; import { Job as ListingJob } from '../report_listing'; @@ -43,7 +43,7 @@ class ReportErrorButtonUi extends Component { public render() { const { record, intl } = this.props; - if (record.status !== JobStatuses.FAILED) { + if (record.status !== JOB_STATUSES.FAILED) { return null; } diff --git a/x-pack/plugins/reporting/public/components/buttons/report_info_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_info_button.tsx index 068cb7d44b0a16..0e249f156f5874 100644 --- a/x-pack/plugins/reporting/public/components/buttons/report_info_button.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/report_info_button.tsx @@ -17,8 +17,8 @@ import { } from '@elastic/eui'; import { get } from 'lodash'; import React, { Component, Fragment } from 'react'; +import { USES_HEADLESS_JOB_TYPES } from '../../../common/constants'; import { ReportApiJSON } from '../../../common/types'; -import { USES_HEADLESS_JOB_TYPES } from '../../../constants'; import { ReportingAPIClient } from '../../lib/reporting_api_client'; interface Props { diff --git a/x-pack/plugins/reporting/public/components/index.ts b/x-pack/plugins/reporting/public/components/index.ts index 354ef189704ad0..370e90c8d2d08f 100644 --- a/x-pack/plugins/reporting/public/components/index.ts +++ b/x-pack/plugins/reporting/public/components/index.ts @@ -9,3 +9,4 @@ export { getFailureToast } from './job_failure'; export { getWarningFormulasToast } from './job_warning_formulas'; export { getWarningMaxSizeToast } from './job_warning_max_size'; export { getGeneralErrorToast } from './general_error'; +export { ScreenCapturePanelContent } from './screen_capture_panel_content'; diff --git a/x-pack/plugins/reporting/public/components/job_download_button.tsx b/x-pack/plugins/reporting/public/components/job_download_button.tsx index 8cf3ce8644add1..7dff2cafa047b2 100644 --- a/x-pack/plugins/reporting/public/components/job_download_button.tsx +++ b/x-pack/plugins/reporting/public/components/job_download_button.tsx @@ -7,8 +7,7 @@ import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { JobSummary } from '../'; -import { JobId } from '../../common/types'; +import { JobId, JobSummary } from '../../common/types'; interface Props { getUrl: (jobId: JobId) => string; diff --git a/x-pack/plugins/reporting/public/components/job_failure.tsx b/x-pack/plugins/reporting/public/components/job_failure.tsx index 8d8f32f692343d..e9c3a448cfe412 100644 --- a/x-pack/plugins/reporting/public/components/job_failure.tsx +++ b/x-pack/plugins/reporting/public/components/job_failure.tsx @@ -9,8 +9,8 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { ToastInput } from 'src/core/public'; -import { JobSummary, ManagementLinkFn } from '../'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; +import { JobSummary, ManagementLinkFn } from '../../common/types'; export const getFailureToast = ( errorText: string, diff --git a/x-pack/plugins/reporting/public/components/job_success.tsx b/x-pack/plugins/reporting/public/components/job_success.tsx index 05cf2c4c5784a6..f03914b2be2f2d 100644 --- a/x-pack/plugins/reporting/public/components/job_success.tsx +++ b/x-pack/plugins/reporting/public/components/job_success.tsx @@ -7,9 +7,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { ToastInput } from 'src/core/public'; -import { JobSummary } from '../'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { JobId } from '../../common/types'; +import { JobId, JobSummary } from '../../common/types'; import { DownloadButton } from './job_download_button'; import { ReportLink } from './report_link'; diff --git a/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx b/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx index 8cccc94e98dcda..338c718a060c1e 100644 --- a/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx +++ b/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx @@ -7,9 +7,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { ToastInput } from 'src/core/public'; -import { JobSummary } from '../'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { JobId } from '../../common/types'; +import { JobId, JobSummary } from '../../common/types'; import { DownloadButton } from './job_download_button'; import { ReportLink } from './report_link'; diff --git a/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx b/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx index c350eef0e5a547..cab743e2006df0 100644 --- a/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx +++ b/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx @@ -7,9 +7,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { ToastInput } from 'src/core/public'; -import { JobSummary } from '../'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { JobId } from '../../common/types'; +import { JobId, JobSummary } from '../../common/types'; import { DownloadButton } from './job_download_button'; import { ReportLink } from './report_link'; diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index a512b1305b8e03..ac6d03a407c289 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -22,9 +22,9 @@ import { Component, default as React, Fragment } from 'react'; import { Subscription } from 'rxjs'; import { ApplicationStart, ToastsSetup } from 'src/core/public'; import { ILicense, LicensingPluginSetup } from '../../../licensing/public'; +import { JOB_STATUSES as JobStatuses } from '../../common/constants'; import { Poller } from '../../common/poller'; import { durationToNumber } from '../../common/schema_utils'; -import { JobStatuses } from '../../constants'; import { checkLicense } from '../lib/license_check'; import { JobQueueEntry, ReportingAPIClient } from '../lib/reporting_api_client'; import { ClientConfigType } from '../plugin'; diff --git a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx index 18895f9e623eb9..7f48b5d9101baa 100644 --- a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx @@ -10,6 +10,7 @@ import React, { Component, ReactElement } from 'react'; import { ToastsSetup } from 'src/core/public'; import url from 'url'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; +import { CSV_REPORT_TYPE, PDF_REPORT_TYPE, PNG_REPORT_TYPE } from '../../common/constants'; import { BaseParams } from '../../common/types'; import { ReportingAPIClient } from '../lib/reporting_api_client'; @@ -165,12 +166,12 @@ class ReportingPanelContentUi extends Component { private prettyPrintReportingType = () => { switch (this.props.reportType) { - case 'printablePdf': + case PDF_REPORT_TYPE: return 'PDF'; case 'csv': - return 'CSV'; + return CSV_REPORT_TYPE; case 'png': - return 'PNG'; + return PNG_REPORT_TYPE; default: return this.props.reportType; } diff --git a/x-pack/plugins/reporting/public/index.ts b/x-pack/plugins/reporting/public/index.ts index 251fd14ee4d57c..f15a5ca4817571 100644 --- a/x-pack/plugins/reporting/public/index.ts +++ b/x-pack/plugins/reporting/public/index.ts @@ -5,33 +5,21 @@ */ import { PluginInitializerContext } from 'src/core/public'; -import { ReportingPublicPlugin } from './plugin'; +import { ScreenCapturePanelContent } from './components/screen_capture_panel_content'; import * as jobCompletionNotifications from './lib/job_completion_notifications'; -import { JobId, JobStatus } from '../common/types'; +import { ReportingAPIClient } from './lib/reporting_api_client'; +import { ReportingPublicPlugin } from './plugin'; -export function plugin(initializerContext: PluginInitializerContext) { - return new ReportingPublicPlugin(initializerContext); +export interface ReportingSetup { + components: { + ScreenCapturePanel: typeof ScreenCapturePanelContent; + }; } -export { ReportingPublicPlugin as Plugin }; -export { jobCompletionNotifications }; +export type ReportingStart = ReportingSetup; -export interface JobSummary { - id: JobId; - status: JobStatus; - title: string; - jobtype: string; - maxSizeReached?: boolean; - csvContainsFormulas?: boolean; -} +export { ReportingAPIClient, ReportingPublicPlugin as Plugin, jobCompletionNotifications }; -export interface JobSummarySet { - completed: JobSummary[]; - failed: JobSummary[]; +export function plugin(initializerContext: PluginInitializerContext) { + return new ReportingPublicPlugin(initializerContext); } - -type DownloadLink = string; -export type DownloadReportFn = (jobId: JobId) => DownloadLink; - -type ManagementLink = string; -export type ManagementLinkFn = () => ManagementLink; diff --git a/x-pack/plugins/reporting/public/lib/job_completion_notifications.ts b/x-pack/plugins/reporting/public/lib/job_completion_notifications.ts index 06694361b757d5..39a7c9f84b8e5b 100644 --- a/x-pack/plugins/reporting/public/lib/job_completion_notifications.ts +++ b/x-pack/plugins/reporting/public/lib/job_completion_notifications.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../../constants'; +import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../../common/constants'; type JobId = string; diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client.ts index 2853caaaaa1b51..71b57d0c0124e3 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client.ts @@ -7,14 +7,20 @@ import { stringify } from 'query-string'; import rison from 'rison-node'; import { HttpSetup } from 'src/core/public'; -import { DownloadReportFn, ManagementLinkFn } from '../'; -import { JobId, ReportApiJSON, ReportDocument, ReportSource } from '../../common/types'; import { API_BASE_GENERATE, API_BASE_URL, API_LIST_URL, REPORTING_MANAGEMENT_HOME, -} from '../../constants'; +} from '../../common/constants'; +import { + DownloadReportFn, + JobId, + ManagementLinkFn, + ReportApiJSON, + ReportDocument, + ReportSource, +} from '../../common/types'; import { add } from './job_completion_notifications'; export interface JobQueueEntry { diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts index f91517e4397f93..31d324bd77159b 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts @@ -6,8 +6,7 @@ import sinon, { stub } from 'sinon'; import { NotificationsStart } from 'src/core/public'; -import { JobSummary } from '../'; -import { ReportDocument } from '../../common/types'; +import { JobSummary, ReportDocument } from '../../common/types'; import { ReportingAPIClient } from './reporting_api_client'; import { ReportingNotifierStreamHandler } from './stream_handler'; diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.ts b/x-pack/plugins/reporting/public/lib/stream_handler.ts index d97c0a7a2d11ef..4b2305b60c413c 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.ts @@ -8,14 +8,8 @@ import { i18n } from '@kbn/i18n'; import * as Rx from 'rxjs'; import { catchError, map } from 'rxjs/operators'; import { NotificationsSetup } from 'src/core/public'; -import { JobSummarySet, JobSummary } from '../'; -import { JobId, ReportDocument } from '../../common/types'; -import { - JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, - JOB_STATUS_COMPLETED, - JOB_STATUS_FAILED, - JOB_STATUS_WARNINGS, -} from '../../constants'; +import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JOB_STATUSES } from '../../common/constants'; +import { JobId, JobSummary, JobSummarySet, ReportDocument } from '../../common/types'; import { getFailureToast, getGeneralErrorToast, @@ -107,9 +101,9 @@ export class ReportingNotifierStreamHandler { _source: { status: jobStatus }, } = job; if (storedJobs.includes(jobId)) { - if (jobStatus === JOB_STATUS_COMPLETED || jobStatus === JOB_STATUS_WARNINGS) { + if (jobStatus === JOB_STATUSES.COMPLETED || jobStatus === JOB_STATUSES.WARNINGS) { completedJobs.push(getReportStatus(job)); - } else if (jobStatus === JOB_STATUS_FAILED) { + } else if (jobStatus === JOB_STATUSES.FAILED) { failedJobs.push(getReportStatus(job)); } else { pending.push(jobId); diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index 1e3f7e34bebdb1..9a4832b114e40d 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -9,20 +9,18 @@ import _ from 'lodash'; import moment from 'moment-timezone'; import { CoreSetup } from 'src/core/public'; import { - UiActionsActionDefinition as ActionDefinition, + ISearchEmbeddable, + SEARCH_EMBEDDABLE_TYPE, +} from '../../../../../src/plugins/discover/public'; +import { IEmbeddable, ViewMode } from '../../../../../src/plugins/embeddable/public'; +import { IncompatibleActionError, + UiActionsActionDefinition as ActionDefinition, } from '../../../../../src/plugins/ui_actions/public'; import { LicensingPluginSetup } from '../../../licensing/public'; +import { API_GENERATE_IMMEDIATE, CSV_REPORTING_ACTION } from '../../common/constants'; import { checkLicense } from '../lib/license_check'; -import { ViewMode, IEmbeddable } from '../../../../../src/plugins/embeddable/public'; -import { - ISearchEmbeddable, - SEARCH_EMBEDDABLE_TYPE, -} from '../../../../../src/plugins/discover/public'; - -import { API_GENERATE_IMMEDIATE, CSV_REPORTING_ACTION } from '../../constants'; - function isSavedSearchEmbeddable( embeddable: IEmbeddable | ISearchEmbeddable ): embeddable is ISearchEmbeddable { diff --git a/x-pack/plugins/reporting/public/plugin.ts b/x-pack/plugins/reporting/public/plugin.ts index 33f4fd4abf72cb..52362b4c68734f 100644 --- a/x-pack/plugins/reporting/public/plugin.ts +++ b/x-pack/plugins/reporting/public/plugin.ts @@ -24,11 +24,14 @@ import { import { ManagementSetup, ManagementStart } from '../../../../src/plugins/management/public'; import { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public'; import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public'; +import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../common/constants'; import { durationToNumber } from '../common/schema_utils'; -import { JobId, ReportingConfigType } from '../common/types'; -import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../constants'; -import { JobSummarySet } from './'; -import { getGeneralErrorToast } from './components'; +import { JobId, JobSummarySet } from '../common/types'; +import { ReportingSetup, ReportingStart } from './'; +import { + getGeneralErrorToast, + ScreenCapturePanelContent as ScreenCapturePanel, +} from './components'; import { ReportingAPIClient } from './lib/reporting_api_client'; import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_handler'; import { GetCsvReportPanelAction } from './panel_actions/get_csv_panel_action'; @@ -36,7 +39,12 @@ import { csvReportingProvider } from './share_context_menu/register_csv_reportin import { reportingPDFPNGProvider } from './share_context_menu/register_pdf_png_reporting'; export interface ClientConfigType { - poll: ReportingConfigType['poll']; + poll: { + jobsRefresh: { + interval: number; + intervalErrorMultiplier: number; + }; + }; } function getStored(): JobId[] { @@ -75,8 +83,13 @@ export interface ReportingPublicPluginStartDendencies { export class ReportingPublicPlugin implements - Plugin { - private config: ClientConfigType; + Plugin< + ReportingSetup, + ReportingStart, + ReportingPublicPluginSetupDendencies, + ReportingPublicPluginStartDendencies + > { + private readonly contract: ReportingStart = { components: { ScreenCapturePanel } }; private readonly stop$ = new Rx.ReplaySubject(1); private readonly title = i18n.translate('xpack.reporting.management.reportingTitle', { defaultMessage: 'Reporting', @@ -84,6 +97,7 @@ export class ReportingPublicPlugin private readonly breadcrumbText = i18n.translate('xpack.reporting.breadcrumb', { defaultMessage: 'Reporting', }); + private config: ClientConfigType; constructor(initializerContext: PluginInitializerContext) { this.config = initializerContext.config.get(); @@ -149,6 +163,8 @@ export class ReportingPublicPlugin uiSettings, }) ); + + return this.contract; } public start(core: CoreStart) { @@ -166,6 +182,8 @@ export class ReportingPublicPlugin catchError((err) => handleError(notifications, err)) ) .subscribe(); + + return this.contract; } public stop() { diff --git a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts index 5b98a198b7d1a6..43243d265e926d 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CSV_JOB_TYPE } from '../../../constants'; +import { CSV_JOB_TYPE } from '../../../common/constants'; import { cryptoFactory } from '../../lib'; import { CreateJobFn, CreateJobFnFactory } from '../../types'; import { IndexPatternSavedObject, JobParamsCSV, TaskPayloadCSV } from './types'; diff --git a/x-pack/plugins/reporting/server/export_types/csv/index.ts b/x-pack/plugins/reporting/server/export_types/csv/index.ts index e66cfef18c6e26..f7b7ff5709fe69 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/index.ts @@ -5,6 +5,7 @@ */ import { + CSV_JOB_TYPE as jobType, LICENSE_TYPE_BASIC, LICENSE_TYPE_ENTERPRISE, LICENSE_TYPE_GOLD, @@ -12,7 +13,6 @@ import { LICENSE_TYPE_STANDARD, LICENSE_TYPE_TRIAL, } from '../../../common/constants'; -import { CSV_JOB_TYPE as jobType } from '../../../constants'; import { CreateJobFn, ExportTypeDefinition, RunTaskFn } from '../../types'; import { createJobFnFactory } from './create_job'; import { runTaskFnFactory } from './execute_job'; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts index abe9fbf3e39506..2c163aeb57a644 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts @@ -5,6 +5,7 @@ */ import { + CSV_FROM_SAVEDOBJECT_JOB_TYPE, LICENSE_TYPE_BASIC, LICENSE_TYPE_ENTERPRISE, LICENSE_TYPE_GOLD, @@ -12,7 +13,6 @@ import { LICENSE_TYPE_STANDARD, LICENSE_TYPE_TRIAL, } from '../../../common/constants'; -import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../constants'; import { ExportTypeDefinition } from '../../types'; import { createJobFnFactory, ImmediateCreateJobFn } from './create_job'; import { ImmediateExecuteFn, runTaskFnFactory } from './execute_job'; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/metadata.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/metadata.ts index a0fd8a29fdcc45..fda360103a1153 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/metadata.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/metadata.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../constants'; +import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; export const metadata = { id: CSV_FROM_SAVEDOBJECT_JOB_TYPE, diff --git a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts index b1fcdbe05fd67f..010b6f431db7ed 100644 --- a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PNG_JOB_TYPE } from '../../../../constants'; +import { PNG_JOB_TYPE } from '../../../../common/constants'; import { cryptoFactory } from '../../../lib'; import { CreateJobFn, CreateJobFnFactory } from '../../../types'; import { validateUrls } from '../../common'; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts index dcd33a0fc8d530..a529cb864b6f73 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PDF_JOB_TYPE } from '../../../../constants'; +import { PDF_JOB_TYPE } from '../../../../common/constants'; import { cryptoFactory } from '../../../lib'; import { CreateJobFn, CreateJobFnFactory } from '../../../types'; import { validateUrls } from '../../common'; diff --git a/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts index e69b8d61dec0d2..c90f67b81317e8 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { LAYOUT_TYPES } from '../../../common/constants'; import { CaptureConfig } from '../../types'; -import { LayoutInstance, LayoutParams, LayoutTypes } from './'; +import { LayoutInstance, LayoutParams } from './'; import { PreserveLayout } from './preserve_layout'; import { PrintLayout } from './print_layout'; @@ -13,7 +14,7 @@ export function createLayout( captureConfig: CaptureConfig, layoutParams?: LayoutParams ): LayoutInstance { - if (layoutParams && layoutParams.dimensions && layoutParams.id === LayoutTypes.PRESERVE_LAYOUT) { + if (layoutParams && layoutParams.dimensions && layoutParams.id === LAYOUT_TYPES.PRESERVE_LAYOUT) { return new PreserveLayout(layoutParams.dimensions); } diff --git a/x-pack/plugins/reporting/server/lib/layouts/index.ts b/x-pack/plugins/reporting/server/lib/layouts/index.ts index c091339a605823..8bfe79aeb8a216 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/index.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/index.ts @@ -4,59 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HeadlessChromiumDriver } from '../../browsers'; import { LevelLogger } from '../'; +import { LayoutSelectorDictionary, Size } from '../../../common/types'; +import { HeadlessChromiumDriver } from '../../browsers'; import { Layout } from './layout'; +export { + LayoutParams, + LayoutSelectorDictionary, + PageSizeParams, + PdfImageSize, + Size, +} from '../../../common/types'; export { createLayout } from './create_layout'; export { Layout } from './layout'; export { PreserveLayout } from './preserve_layout'; export { PrintLayout } from './print_layout'; -export const LayoutTypes = { - PRESERVE_LAYOUT: 'preserve_layout', - PRINT: 'print', -}; - -export const getDefaultLayoutSelectors = (): LayoutSelectorDictionary => ({ - screenshot: '[data-shared-items-container]', - renderComplete: '[data-shared-item]', - itemsCountAttribute: 'data-shared-items-count', - timefilterDurationAttribute: 'data-shared-timefilter-duration', -}); - -export interface PageSizeParams { - pageMarginTop: number; - pageMarginBottom: number; - pageMarginWidth: number; - tableBorderWidth: number; - headingHeight: number; - subheadingHeight: number; -} - -export interface LayoutSelectorDictionary { - screenshot: string; - renderComplete: string; - itemsCountAttribute: string; - timefilterDurationAttribute: string; -} - -export interface PdfImageSize { - width: number; - height?: number; -} - -export interface Size { - width: number; - height: number; -} - -export interface LayoutParams { - id: string; - dimensions?: Size; - selectors?: LayoutSelectorDictionary; -} - interface LayoutSelectors { // Fields that are not part of Layout: the instances // independently implement these fields on their own diff --git a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts index faddaae64ce5dd..549e898d8a13ea 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts @@ -6,15 +6,10 @@ import path from 'path'; import { CustomPageSize } from 'pdfmake/interfaces'; -import { - getDefaultLayoutSelectors, - Layout, - LayoutInstance, - LayoutSelectorDictionary, - LayoutTypes, - PageSizeParams, - Size, -} from './'; +import { getDefaultLayoutSelectors } from '../../../common'; +import { LAYOUT_TYPES } from '../../../common/constants'; +import { LayoutSelectorDictionary, PageSizeParams, Size } from '../../../common/types'; +import { Layout, LayoutInstance } from './'; // We use a zoom of two to bump up the resolution of the screenshot a bit. const ZOOM: number = 2; @@ -28,7 +23,7 @@ export class PreserveLayout extends Layout implements LayoutInstance { private readonly scaledWidth: number; constructor(size: Size, layoutSelectors?: LayoutSelectorDictionary) { - super(LayoutTypes.PRESERVE_LAYOUT); + super(LAYOUT_TYPES.PRESERVE_LAYOUT); this.height = size.height; this.width = size.width; this.scaledHeight = size.height * ZOOM; diff --git a/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts index e979cdeeb71fec..8db1fa7ff63478 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts @@ -8,16 +8,12 @@ import path from 'path'; import { PageOrientation, PredefinedPageSize } from 'pdfmake/interfaces'; import { EvaluateFn, SerializableOrJSHandle } from 'puppeteer'; import { LevelLogger } from '../'; +import { getDefaultLayoutSelectors } from '../../../common'; +import { LAYOUT_TYPES } from '../../../common/constants'; +import { LayoutSelectorDictionary, Size } from '../../../common/types'; import { HeadlessChromiumDriver } from '../../browsers'; import { CaptureConfig } from '../../types'; -import { - getDefaultLayoutSelectors, - LayoutInstance, - LayoutSelectorDictionary, - LayoutTypes, - Size, -} from './'; -import { Layout } from './layout'; +import { Layout, LayoutInstance } from './'; export class PrintLayout extends Layout implements LayoutInstance { public readonly selectors: LayoutSelectorDictionary = { @@ -28,7 +24,7 @@ export class PrintLayout extends Layout implements LayoutInstance { private captureConfig: CaptureConfig; constructor(captureConfig: CaptureConfig) { - super(LayoutTypes.PRINT); + super(LAYOUT_TYPES.PRINT); this.captureConfig = captureConfig; } diff --git a/x-pack/plugins/reporting/server/lib/store/index.ts b/x-pack/plugins/reporting/server/lib/store/index.ts index a48f2661203236..17f0fb5bf03893 100644 --- a/x-pack/plugins/reporting/server/lib/store/index.ts +++ b/x-pack/plugins/reporting/server/lib/store/index.ts @@ -4,5 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export { Report, ReportDocument } from './report'; +export { ReportDocument } from '../../../common/types'; +export { Report } from './report'; export { ReportingStore } from './store'; diff --git a/x-pack/plugins/reporting/server/lib/store/report.ts b/x-pack/plugins/reporting/server/lib/store/report.ts index d82b90f4025ed2..2e4473ef8f2ea6 100644 --- a/x-pack/plugins/reporting/server/lib/store/report.ts +++ b/x-pack/plugins/reporting/server/lib/store/report.ts @@ -7,51 +7,8 @@ import moment from 'moment'; // @ts-ignore no module definition import Puid from 'puid'; -import { JobStatus, ReportApiJSON } from '../../../common/types'; -import { JobStatuses } from '../../../constants'; -import { LayoutParams } from '../layouts'; -import { TaskRunResult } from '../tasks'; - -interface ReportDocumentHead { - _id: string; - _index: string; - _seq_no: unknown; - _primary_term: unknown; -} - -/* - * The document created by Reporting to store in the .reporting index - */ -export interface ReportDocument extends ReportDocumentHead { - _source: ReportSource; -} - -export interface ReportSource { - jobtype: string; - kibana_name: string; - kibana_id: string; - created_by: string | false; - payload: { - headers: string; // encrypted headers - browserTimezone?: string; // may use timezone from advanced settings - objectType: string; - title: string; - layout?: LayoutParams; - }; - meta: { objectType: string; layout?: string }; - browser_type: string; - max_attempts: number; - timeout: number; - - status: JobStatus; - attempts: number; - output: TaskRunResult | null; - started_at?: string; - completed_at?: string; - created_at: string; - priority?: number; - process_expiration?: string; -} +import { JOB_STATUSES } from '../../../common/constants'; +import { ReportApiJSON, ReportDocumentHead, ReportSource } from '../../../common/types'; const puid = new Puid(); @@ -107,7 +64,7 @@ export class Report implements Partial { this.browser_type = opts.browser_type; this.priority = opts.priority; - this.status = opts.status || JobStatuses.PENDING; + this.status = opts.status || JOB_STATUSES.PENDING; this.output = opts.output || null; } @@ -175,3 +132,5 @@ export class Report implements Partial { }; } } + +export { ReportApiJSON, ReportSource }; diff --git a/x-pack/plugins/reporting/server/lib/tasks/index.ts b/x-pack/plugins/reporting/server/lib/tasks/index.ts index 0dd9945985bfb5..c866c81c9793c0 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/index.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/index.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ReportSource, TaskRunResult } from '../../../common/types'; import { BasePayload } from '../../types'; -import { ReportSource } from '../store/report'; /* * The document created by Reporting to store as task parameters for Task @@ -22,11 +22,4 @@ export interface ReportTaskParams { meta: ReportSource['meta']; } -export interface TaskRunResult { - content_type: string | null; - content: string | null; - csv_contains_formulas?: boolean; - size: number; - max_size_reached?: boolean; - warnings?: string[]; -} +export { TaskRunResult }; diff --git a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts index 400fbb16f54dcb..6ac5875acd34c6 100644 --- a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts @@ -7,7 +7,6 @@ import { schema } from '@kbn/config-schema'; import { KibanaRequest } from 'src/core/server'; import { ReportingCore } from '../'; -import { API_BASE_GENERATE_V1 } from '../../common/constants'; import { createJobFnFactory } from '../export_types/csv_from_savedobject/create_job'; import { runTaskFnFactory } from '../export_types/csv_from_savedobject/execute_job'; import { @@ -20,6 +19,9 @@ import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routi import { getJobParamsFromRequest } from './lib/get_job_params_from_request'; import { HandlerErrorFunction } from './types'; +const API_BASE_URL_V1 = '/api/reporting/v1'; +const API_BASE_GENERATE_V1 = `${API_BASE_URL_V1}/generate`; + export type CsvFromSavedObjectRequest = KibanaRequest< JobParamsPanelCsv, unknown, diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts index c9dbbda9fd68de..12a3ac5c762c74 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createLayout, LayoutInstance, LayoutTypes } from '../lib/layouts'; +import { LAYOUT_TYPES } from '../../common/constants'; +import { createLayout, LayoutInstance } from '../lib/layouts'; import { CaptureConfig } from '../types'; export const createMockLayoutInstance = (captureConfig: CaptureConfig) => { const mockLayout = createLayout(captureConfig, { - id: LayoutTypes.PRESERVE_LAYOUT, + id: LAYOUT_TYPES.PRESERVE_LAYOUT, dimensions: { height: 100, width: 100 }, }) as LayoutInstance; mockLayout.selectors = { diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index eb046a3eab0752..8cd26df032f641 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -8,15 +8,15 @@ import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { DataPluginStart } from 'src/plugins/data/server/plugin'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { SpacesPluginSetup } from '../../spaces/server'; -import { CancellationToken } from '../../../plugins/reporting/common'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { AuthenticatedUser, SecurityPluginSetup } from '../../security/server'; +import { SpacesPluginSetup } from '../../spaces/server'; +import { CancellationToken } from '../common'; +import { BaseParams } from '../common/types'; import { ReportingConfigType } from './config'; import { ReportingCore } from './core'; import { LevelLogger } from './lib'; -import { LayoutParams } from './lib/layouts'; import { ReportTaskParams, TaskRunResult } from './lib/tasks'; /* @@ -47,12 +47,7 @@ export type ReportingUser = { username: AuthenticatedUser['username'] } | false; export type CaptureConfig = ReportingConfigType['capture']; export type ScrollConfig = ReportingConfigType['csv']['scroll']; -export interface BaseParams { - browserTimezone?: string; // browserTimezone is optional: it is not in old POST URLs that were generated prior to being added to this interface - layout?: LayoutParams; - objectType: string; - title: string; -} +export { BaseParams }; // base params decorated with encrypted headers that come into runJob functions export interface BasePayload extends BaseParams { From 208e86e66a7001a1f93f0dd0d937af5cc4deb9db Mon Sep 17 00:00:00 2001 From: John Schulz Date: Thu, 12 Nov 2020 11:05:17 -0500 Subject: [PATCH 28/52] [Ingest Manager] Lift up registry/{stream,extract} functions (#83239) ## Summary * Move stream utility functions from `server/services/epm/registry/streams.ts` to `server/services/epm/streams.ts` * They're only used in registry at the moment but aren't specific to registry * Move archive extraction functions from `server/services/epm/registry/extract.ts` to `server/services/epm/archive.ts` * The Registry isn't the only service/code which needs to extract packages. Continue consolidating archive-related code under archive vs registry --- .../server/services/epm/{registry => archive}/extract.ts | 8 ++------ x-pack/plugins/fleet/server/services/epm/archive/index.ts | 8 +++++++- .../fleet/server/services/epm/archive/validation.ts | 3 ++- .../services/epm/elasticsearch/ingest_pipeline/install.ts | 3 +-- .../plugins/fleet/server/services/epm/packages/assets.ts | 6 +++--- .../fleet/server/services/epm/registry/index.test.ts | 5 ++--- .../plugins/fleet/server/services/epm/registry/index.ts | 4 +--- .../fleet/server/services/epm/registry/requests.ts | 2 +- .../fleet/server/services/epm/{registry => }/streams.ts | 0 9 files changed, 19 insertions(+), 20 deletions(-) rename x-pack/plugins/fleet/server/services/epm/{registry => archive}/extract.ts (95%) rename x-pack/plugins/fleet/server/services/epm/{registry => }/streams.ts (100%) diff --git a/x-pack/plugins/fleet/server/services/epm/registry/extract.ts b/x-pack/plugins/fleet/server/services/epm/archive/extract.ts similarity index 95% rename from x-pack/plugins/fleet/server/services/epm/registry/extract.ts rename to x-pack/plugins/fleet/server/services/epm/archive/extract.ts index b79218638ce247..6ac81a25dfc21e 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/extract.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/extract.ts @@ -6,12 +6,8 @@ import tar from 'tar'; import yauzl from 'yauzl'; -import { bufferToStream, streamToBuffer } from './streams'; - -export interface ArchiveEntry { - path: string; - buffer?: Buffer; -} +import { bufferToStream, streamToBuffer } from '../streams'; +import { ArchiveEntry } from './index'; export async function untarBuffer( buffer: Buffer, diff --git a/x-pack/plugins/fleet/server/services/epm/archive/index.ts b/x-pack/plugins/fleet/server/services/epm/archive/index.ts index 810740d697fcbc..6d1150b3ac8bd5 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/index.ts @@ -14,10 +14,16 @@ import { setArchiveFilelist, deleteArchiveFilelist, } from './cache'; -import { ArchiveEntry, getBufferExtractor } from '../registry/extract'; +import { getBufferExtractor } from './extract'; import { parseAndVerifyArchiveEntries } from './validation'; export * from './cache'; +export { untarBuffer, unzipBuffer, getBufferExtractor } from './extract'; + +export interface ArchiveEntry { + path: string; + buffer?: Buffer; +} export async function getArchivePackage({ archiveBuffer, diff --git a/x-pack/plugins/fleet/server/services/epm/archive/validation.ts b/x-pack/plugins/fleet/server/services/epm/archive/validation.ts index d9d451544a9538..992020cb073add 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/validation.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/validation.ts @@ -15,7 +15,8 @@ import { RegistryVarsEntry, } from '../../../../common/types'; import { PackageInvalidArchiveError } from '../../../errors'; -import { ArchiveEntry, pkgToPkgKey } from '../registry'; +import { ArchiveEntry } from './index'; +import { pkgToPkgKey } from '../registry'; const MANIFESTS: Record = {}; const MANIFEST_NAME = 'manifest.yml'; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts index c5c9e8ac2c01ba..b6988f64843d09 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts @@ -11,8 +11,7 @@ import { ElasticsearchAssetType, InstallablePackage, } from '../../../../types'; -import { ArchiveEntry } from '../../registry'; -import { getAsset, getPathParts } from '../../archive'; +import { ArchiveEntry, getAsset, getPathParts } from '../../archive'; import { CallESAsCurrentUser } from '../../../../types'; import { saveInstalledEsRefs } from '../../packages/install'; import { getInstallationObject } from '../../packages'; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/assets.ts b/x-pack/plugins/fleet/server/services/epm/packages/assets.ts index 50d8f2f4d2fb22..80e1cbba6484b6 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/assets.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/assets.ts @@ -6,7 +6,7 @@ import { InstallablePackage } from '../../../types'; import * as Registry from '../registry'; -import { getArchiveFilelist, getAsset } from '../archive'; +import { ArchiveEntry, getArchiveFilelist, getAsset } from '../archive'; // paths from RegistryPackage are routes to the assets on EPR // e.g. `/package/nginx/1.2.0/data_stream/access/fields/fields.yml` @@ -51,14 +51,14 @@ export async function getAssetsData( packageInfo: InstallablePackage, filter = (path: string): boolean => true, datasetName?: string -): Promise { +): Promise { // TODO: Needs to be called to fill the cache but should not be required await Registry.ensureCachedArchiveInfo(packageInfo.name, packageInfo.version, 'registry'); // Gather all asset data const assets = getAssets(packageInfo, filter, datasetName); - const entries: Registry.ArchiveEntry[] = assets.map((path) => { + const entries: ArchiveEntry[] = assets.map((path) => { const buffer = getAsset(path); return { path, buffer }; diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts index 1208ffdaefe4af..aea28b5d56ab9d 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts @@ -5,9 +5,8 @@ */ import { AssetParts } from '../../../types'; -import { getPathParts } from '../archive'; -import { getBufferExtractor, splitPkgKey } from './index'; -import { untarBuffer, unzipBuffer } from './extract'; +import { getBufferExtractor, getPathParts, untarBuffer, unzipBuffer } from '../archive'; +import { splitPkgKey } from './index'; const testPaths = [ { diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.ts index c35e91bdf580bd..aef1bb75619cdf 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.ts @@ -24,13 +24,11 @@ import { unpackArchiveToCache, } from '../archive'; import { fetchUrl, getResponse, getResponseStream } from './requests'; -import { streamToBuffer } from './streams'; +import { streamToBuffer } from '../streams'; import { getRegistryUrl } from './registry_url'; import { appContextService } from '../..'; import { PackageNotFoundError, PackageCacheError } from '../../../errors'; -export { ArchiveEntry, getBufferExtractor } from './extract'; - export interface SearchParams { category?: CategoryId; experimental?: boolean; diff --git a/x-pack/plugins/fleet/server/services/epm/registry/requests.ts b/x-pack/plugins/fleet/server/services/epm/registry/requests.ts index 2b9c3495657903..c8d158c8afaaaa 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/requests.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/requests.ts @@ -6,7 +6,7 @@ import fetch, { FetchError, Response, RequestInit } from 'node-fetch'; import pRetry from 'p-retry'; -import { streamToString } from './streams'; +import { streamToString } from '../streams'; import { appContextService } from '../../app_context'; import { RegistryError, RegistryConnectionError, RegistryResponseError } from '../../../errors'; import { getProxyAgent, getRegistryProxyUrl } from './proxy'; diff --git a/x-pack/plugins/fleet/server/services/epm/registry/streams.ts b/x-pack/plugins/fleet/server/services/epm/streams.ts similarity index 100% rename from x-pack/plugins/fleet/server/services/epm/registry/streams.ts rename to x-pack/plugins/fleet/server/services/epm/streams.ts From ab72206da338e81e549da128e6ca5fd7a30e2b30 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 12 Nov 2020 16:39:40 +0000 Subject: [PATCH 29/52] [Alerting] Moves the Index & Geo Threshold UIs into the Stack Alerts Public Plugin (#82951) This PR includes the following refactors: 1. Moves the Index Pattern Api from _Stack Alerts_ to the _Server_ plugin of _Trigger Actions UI_. This fixes a potential bug where a user could disable the _Stack Alerts_ plugin and inadvertently break the UI of the _ES Index _ action type. 2. Extracts the UI components for _Index Threshold_ and _Geo Threshold_ from the _Trigger Actions UI_ plugin and moves them into _Stack Alerts_. --- .eslintrc.js | 9 +- packages/kbn-optimizer/limits.yml | 1 + .../stack_alerts/{server => common}/config.ts | 1 + x-pack/plugins/stack_alerts/common/index.ts | 2 +- x-pack/plugins/stack_alerts/kibana.json | 4 +- .../alert_types}/geo_threshold/index.ts | 7 +- .../expressions/boundary_index_expression.tsx | 17 +- .../expressions/entity_by_expression.tsx | 8 +- .../expressions/entity_index_expression.tsx | 24 +- .../geo_threshold/query_builder/index.tsx | 37 ++- .../expression_with_popover.tsx | 12 +- .../geo_index_pattern_select.tsx | 26 +- .../util_components/single_field_select.tsx | 2 +- .../alert_types}/geo_threshold/types.ts | 0 .../geo_threshold/validation.test.ts | 0 .../alert_types}/geo_threshold/validation.ts | 18 +- .../public/alert_types}/index.ts | 15 +- .../alert_types}/threshold/expression.scss | 0 .../alert_types}/threshold/expression.tsx | 65 +++-- .../public/alert_types}/threshold/index.ts | 9 +- .../threshold/index_threshold_api.ts | 45 ++++ .../public/alert_types}/threshold/types.ts | 0 .../alert_types}/threshold/validation.test.ts | 0 .../alert_types}/threshold/validation.ts | 22 +- .../alert_types}/threshold/visualization.tsx | 40 +-- x-pack/plugins/stack_alerts/public/index.ts | 10 + x-pack/plugins/stack_alerts/public/plugin.tsx | 35 +++ .../alert_types/geo_threshold/alert_type.ts | 6 +- .../geo_threshold/es_query_builder.ts | 2 +- .../geo_threshold/geo_threshold.ts | 4 +- .../server/alert_types/geo_threshold/index.ts | 9 +- .../geo_threshold/tests/alert_type.test.ts | 6 +- .../stack_alerts/server/alert_types/index.ts | 8 +- .../alert_types/index_threshold/README.md | 230 +----------------- .../index_threshold/alert_type.test.ts | 10 +- .../alert_types/index_threshold/alert_type.ts | 19 +- .../index_threshold/alert_type_params.test.ts | 186 +++++++++++++- .../index_threshold/alert_type_params.ts | 5 +- .../alert_types/index_threshold/index.ts | 24 +- x-pack/plugins/stack_alerts/server/index.ts | 19 +- .../stack_alerts/server/plugin.test.ts | 29 --- x-pack/plugins/stack_alerts/server/plugin.ts | 31 +-- x-pack/plugins/stack_alerts/server/types.ts | 17 +- .../translations/translations/ja-JP.json | 150 ++++++------ .../translations/translations/zh-CN.json | 150 ++++++------ x-pack/plugins/triggers_actions_ui/README.md | 2 +- .../common/data}/index.ts | 0 .../triggers_actions_ui/common/index.ts | 7 + .../plugins/triggers_actions_ui/kibana.json | 4 +- .../public/application/boot.tsx | 2 +- .../public/common/index.ts | 4 + .../public/common/index_controls/index.ts | 19 +- .../public/common/lib/data_apis.ts | 67 +++++ .../public/common/lib/index.ts | 6 + .../public/common/lib/index_threshold_api.ts | 91 ------- .../triggers_actions_ui/public/index.ts | 7 +- .../triggers_actions_ui/public/plugin.ts | 23 +- .../triggers_actions_ui/server/data/README.md | 228 +++++++++++++++++ .../triggers_actions_ui/server/data/index.ts | 40 +++ .../server/data}/lib/core_query_types.test.ts | 0 .../server/data}/lib/core_query_types.ts | 67 +++-- .../server/data}/lib/date_range_info.test.ts | 0 .../server/data}/lib/date_range_info.ts | 15 +- .../server/data/lib/index.ts | 12 + .../data}/lib/time_series_query.test.ts | 21 +- .../server/data}/lib/time_series_query.ts | 5 +- .../data}/lib/time_series_types.test.ts | 0 .../server/data}/lib/time_series_types.ts | 34 +-- .../server/data}/routes/fields.ts | 12 +- .../server/data}/routes/index.ts | 15 +- .../server/data}/routes/indices.ts | 18 +- .../server/data}/routes/time_series_query.ts | 22 +- .../triggers_actions_ui/server/index.ts | 19 +- .../triggers_actions_ui/server/plugin.ts | 39 +++ .../index_threshold/alert.ts | 7 +- .../index_threshold/fields_endpoint.ts | 2 +- .../index_threshold/indices_endpoint.ts | 2 +- .../time_series_query_endpoint.ts | 4 +- 78 files changed, 1212 insertions(+), 896 deletions(-) rename x-pack/plugins/stack_alerts/{server => common}/config.ts (85%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/geo_threshold/index.ts (75%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/geo_threshold/query_builder/expressions/boundary_index_expression.tsx (85%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/geo_threshold/query_builder/expressions/entity_by_expression.tsx (87%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/geo_threshold/query_builder/expressions/entity_index_expression.tsx (83%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/geo_threshold/query_builder/index.tsx (89%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/geo_threshold/query_builder/util_components/expression_with_popover.tsx (88%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/geo_threshold/query_builder/util_components/geo_index_pattern_select.tsx (80%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/geo_threshold/query_builder/util_components/single_field_select.tsx (96%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/geo_threshold/types.ts (100%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/geo_threshold/validation.test.ts (100%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/geo_threshold/validation.ts (72%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/index.ts (57%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/threshold/expression.scss (100%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/threshold/expression.tsx (86%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/threshold/index.ts (77%) create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/threshold/index_threshold_api.ts rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/threshold/types.ts (100%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/threshold/validation.test.ts (100%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/threshold/validation.ts (81%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/threshold/visualization.tsx (86%) create mode 100644 x-pack/plugins/stack_alerts/public/index.ts create mode 100644 x-pack/plugins/stack_alerts/public/plugin.tsx rename x-pack/plugins/{stack_alerts/common/alert_types/index_threshold => triggers_actions_ui/common/data}/index.ts (100%) create mode 100644 x-pack/plugins/triggers_actions_ui/common/index.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/common/lib/index.ts delete mode 100644 x-pack/plugins/triggers_actions_ui/public/common/lib/index_threshold_api.ts create mode 100644 x-pack/plugins/triggers_actions_ui/server/data/README.md create mode 100644 x-pack/plugins/triggers_actions_ui/server/data/index.ts rename x-pack/plugins/{stack_alerts/server/alert_types/index_threshold => triggers_actions_ui/server/data}/lib/core_query_types.test.ts (100%) rename x-pack/plugins/{stack_alerts/server/alert_types/index_threshold => triggers_actions_ui/server/data}/lib/core_query_types.ts (68%) rename x-pack/plugins/{stack_alerts/server/alert_types/index_threshold => triggers_actions_ui/server/data}/lib/date_range_info.test.ts (100%) rename x-pack/plugins/{stack_alerts/server/alert_types/index_threshold => triggers_actions_ui/server/data}/lib/date_range_info.ts (89%) create mode 100644 x-pack/plugins/triggers_actions_ui/server/data/lib/index.ts rename x-pack/plugins/{stack_alerts/server/alert_types/index_threshold => triggers_actions_ui/server/data}/lib/time_series_query.test.ts (58%) rename x-pack/plugins/{stack_alerts/server/alert_types/index_threshold => triggers_actions_ui/server/data}/lib/time_series_query.ts (96%) rename x-pack/plugins/{stack_alerts/server/alert_types/index_threshold => triggers_actions_ui/server/data}/lib/time_series_types.test.ts (100%) rename x-pack/plugins/{stack_alerts/server/alert_types/index_threshold => triggers_actions_ui/server/data}/lib/time_series_types.ts (81%) rename x-pack/plugins/{stack_alerts/server/alert_types/index_threshold => triggers_actions_ui/server/data}/routes/fields.ts (90%) rename x-pack/plugins/{stack_alerts/server/alert_types/index_threshold => triggers_actions_ui/server/data}/routes/index.ts (55%) rename x-pack/plugins/{stack_alerts/server/alert_types/index_threshold => triggers_actions_ui/server/data}/routes/indices.ts (85%) rename x-pack/plugins/{stack_alerts/server/alert_types/index_threshold => triggers_actions_ui/server/data}/routes/time_series_query.ts (58%) create mode 100644 x-pack/plugins/triggers_actions_ui/server/plugin.ts diff --git a/.eslintrc.js b/.eslintrc.js index 561e9bc55bf9d2..ad9de04251e4c0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1035,12 +1035,19 @@ module.exports = { * Alerting Services overrides */ { - // typescript only for front and back end + // typescript for front and back end files: ['x-pack/plugins/{alerts,stack_alerts,actions,task_manager,event_log}/**/*.{ts,tsx}'], rules: { '@typescript-eslint/no-explicit-any': 'error', }, }, + { + // typescript only for back end + files: ['x-pack/plugins/triggers_actions_ui/server/**/*.ts'], + rules: { + '@typescript-eslint/no-explicit-any': 'error', + }, + }, /** * Lens overrides diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 701b7cab21600d..e326c8e2cac39e 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -102,3 +102,4 @@ pageLoadAssetSize: visualizations: 295025 visualize: 57431 watcher: 43598 + stackAlerts: 29684 diff --git a/x-pack/plugins/stack_alerts/server/config.ts b/x-pack/plugins/stack_alerts/common/config.ts similarity index 85% rename from x-pack/plugins/stack_alerts/server/config.ts rename to x-pack/plugins/stack_alerts/common/config.ts index 8a13aedd5fdd85..2e997ce0ebad66 100644 --- a/x-pack/plugins/stack_alerts/server/config.ts +++ b/x-pack/plugins/stack_alerts/common/config.ts @@ -8,6 +8,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), + enableGeoTrackingThresholdAlert: schema.boolean({ defaultValue: false }), }); export type Config = TypeOf; diff --git a/x-pack/plugins/stack_alerts/common/index.ts b/x-pack/plugins/stack_alerts/common/index.ts index 79dd18d321f078..a75625d0641aa1 100644 --- a/x-pack/plugins/stack_alerts/common/index.ts +++ b/x-pack/plugins/stack_alerts/common/index.ts @@ -3,5 +3,5 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +export * from './config'; export const STACK_ALERTS_FEATURE_ID = 'stackAlerts'; diff --git a/x-pack/plugins/stack_alerts/kibana.json b/x-pack/plugins/stack_alerts/kibana.json index b26114577c4302..b7405c38d16117 100644 --- a/x-pack/plugins/stack_alerts/kibana.json +++ b/x-pack/plugins/stack_alerts/kibana.json @@ -3,7 +3,7 @@ "server": true, "version": "8.0.0", "kibanaVersion": "kibana", - "requiredPlugins": ["alerts", "features"], + "requiredPlugins": ["alerts", "features", "triggersActionsUi", "kibanaReact"], "configPath": ["xpack", "stack_alerts"], - "ui": false + "ui": true } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/index.ts b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/index.ts similarity index 75% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/index.ts rename to x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/index.ts index 00d9ebbbbc0660..35f5648de40f35 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/index.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/index.ts @@ -5,18 +5,17 @@ */ import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; -import { AlertTypeModel } from '../../../../types'; import { validateExpression } from './validation'; import { GeoThresholdAlertParams } from './types'; -import { AlertsContextValue } from '../../../context/alerts_context'; +import { AlertTypeModel, AlertsContextValue } from '../../../../triggers_actions_ui/public'; export function getAlertType(): AlertTypeModel { return { id: '.geo-threshold', - name: i18n.translate('xpack.triggersActionsUI.geoThreshold.name.trackingThreshold', { + name: i18n.translate('xpack.stackAlerts.geoThreshold.name.trackingThreshold', { defaultMessage: 'Tracking threshold', }), - description: i18n.translate('xpack.triggersActionsUI.geoThreshold.descriptionText', { + description: i18n.translate('xpack.stackAlerts.geoThreshold.descriptionText', { defaultMessage: 'Alert when an entity enters or leaves a geo boundary.', }), iconClass: 'globe', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/expressions/boundary_index_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/boundary_index_expression.tsx similarity index 85% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/expressions/boundary_index_expression.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/boundary_index_expression.tsx index 497e053a4ed604..55dfc82bdbdb8c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/expressions/boundary_index_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/boundary_index_expression.tsx @@ -7,14 +7,13 @@ import React, { Fragment, FunctionComponent, useEffect, useRef } from 'react'; import { EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { IErrorObject } from '../../../../../../types'; +import { IErrorObject, AlertsContextValue } from '../../../../../../triggers_actions_ui/public'; import { ES_GEO_SHAPE_TYPES, GeoThresholdAlertParams } from '../../types'; -import { AlertsContextValue } from '../../../../../context/alerts_context'; import { GeoIndexPatternSelect } from '../util_components/geo_index_pattern_select'; import { SingleFieldSelect } from '../util_components/single_field_select'; import { ExpressionWithPopover } from '../util_components/expression_with_popover'; -import { IFieldType } from '../../../../../../../../../../src/plugins/data/common/index_patterns/fields'; -import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/common/index_patterns'; +import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; interface Props { alertParams: GeoThresholdAlertParams; @@ -117,12 +116,12 @@ export const BoundaryIndexExpression: FunctionComponent = ({ = ({ = ({ defaultValue={'Select an index pattern and geo shape field'} value={boundaryIndexPattern.title} popoverContent={indexPopover} - expressionDescription={i18n.translate('xpack.triggersActionsUI.geoThreshold.indexLabel', { + expressionDescription={i18n.translate('xpack.stackAlerts.geoThreshold.indexLabel', { defaultMessage: 'index', })} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/expressions/entity_by_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_by_expression.tsx similarity index 87% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/expressions/entity_by_expression.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_by_expression.tsx index f355d25796b7c8..f519ad882802c5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/expressions/entity_by_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_by_expression.tsx @@ -8,10 +8,10 @@ import React, { FunctionComponent, useEffect, useRef } from 'react'; import { EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import _ from 'lodash'; -import { IErrorObject } from '../../../../../../types'; +import { IErrorObject } from '../../../../../../triggers_actions_ui/public'; import { SingleFieldSelect } from '../util_components/single_field_select'; import { ExpressionWithPopover } from '../util_components/expression_with_popover'; -import { IFieldType } from '../../../../../../../../../../src/plugins/data/common/index_patterns/fields'; +import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; interface Props { errors: IErrorObject; @@ -59,7 +59,7 @@ export const EntityByExpression: FunctionComponent = ({ = ({ value={entity} defaultValue={'Select entity field'} popoverContent={indexPopover} - expressionDescription={i18n.translate('xpack.triggersActionsUI.geoThreshold.entityByLabel', { + expressionDescription={i18n.translate('xpack.stackAlerts.geoThreshold.entityByLabel', { defaultMessage: 'by', })} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx similarity index 83% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx index 506530c171cd44..e5e43210d1e6b1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx @@ -8,14 +8,13 @@ import React, { Fragment, FunctionComponent, useEffect, useRef } from 'react'; import { EuiFormRow } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { IErrorObject } from '../../../../../../types'; +import { IErrorObject, AlertsContextValue } from '../../../../../../triggers_actions_ui/public'; import { ES_GEO_FIELD_TYPES } from '../../types'; -import { AlertsContextValue } from '../../../../../context/alerts_context'; import { GeoIndexPatternSelect } from '../util_components/geo_index_pattern_select'; import { SingleFieldSelect } from '../util_components/single_field_select'; import { ExpressionWithPopover } from '../util_components/expression_with_popover'; -import { IFieldType } from '../../../../../../../../../../src/plugins/data/common/index_patterns/fields'; -import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/common/index_patterns'; +import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; interface Props { dateField: string; @@ -105,13 +104,13 @@ export const EntityIndexExpression: FunctionComponent = ({ fullWidth label={ } > = ({ = ({ value={indexPattern.title} defaultValue={'Select an index pattern and geo shape/point field'} popoverContent={indexPopover} - expressionDescription={i18n.translate( - 'xpack.triggersActionsUI.geoThreshold.entityIndexLabel', - { - defaultMessage: 'index', - } - )} + expressionDescription={i18n.translate('xpack.stackAlerts.geoThreshold.entityIndexLabel', { + defaultMessage: 'index', + })} /> ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/index.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx similarity index 89% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/index.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx index ccc2ddd9c01ca8..f138c08c0f9939 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/index.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx @@ -19,15 +19,17 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { AlertTypeParamsExpressionProps } from '../../../../../types'; +import { + AlertTypeParamsExpressionProps, + getTimeOptions, + AlertsContextValue, +} from '../../../../../triggers_actions_ui/public'; import { GeoThresholdAlertParams, TrackingEvent } from '../types'; -import { AlertsContextValue } from '../../../../context/alerts_context'; import { ExpressionWithPopover } from './util_components/expression_with_popover'; import { EntityIndexExpression } from './expressions/entity_index_expression'; import { EntityByExpression } from './expressions/entity_by_expression'; import { BoundaryIndexExpression } from './expressions/boundary_index_expression'; -import { IIndexPattern } from '../../../../../../../../../src/plugins/data/common/index_patterns'; -import { getTimeOptions } from '../../../../../common/lib/get_time_options'; +import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns'; const DEFAULT_VALUES = { TRACKING_EVENT: '', @@ -45,20 +47,20 @@ const DEFAULT_VALUES = { }; const conditionOptions = Object.keys(TrackingEvent).map((key) => ({ - text: (TrackingEvent as any)[key], - value: (TrackingEvent as any)[key], + text: TrackingEvent[key as TrackingEvent], + value: TrackingEvent[key as TrackingEvent], })); const labelForDelayOffset = ( <> {' '} @@ -125,7 +127,7 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent

@@ -221,7 +223,7 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent
@@ -251,7 +253,7 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent
@@ -280,19 +282,16 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent } - expressionDescription={i18n.translate( - 'xpack.triggersActionsUI.geoThreshold.whenEntityLabel', - { - defaultMessage: 'when entity', - } - )} + expressionDescription={i18n.translate('xpack.stackAlerts.geoThreshold.whenEntityLabel', { + defaultMessage: 'when entity', + })} />
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/util_components/expression_with_popover.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/expression_with_popover.tsx similarity index 88% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/util_components/expression_with_popover.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/expression_with_popover.tsx index 7e1cae51f1411a..a83667cfd92c64 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/util_components/expression_with_popover.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/expression_with_popover.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; +import React, { ReactNode, useState } from 'react'; import { EuiButtonIcon, EuiExpression, @@ -22,10 +22,10 @@ export const ExpressionWithPopover: ({ value, isInvalid, }: { - popoverContent: any; - expressionDescription: any; - defaultValue?: any; - value?: any; + popoverContent: ReactNode; + expressionDescription: ReactNode; + defaultValue?: ReactNode; + value?: ReactNode; isInvalid?: boolean; }) => JSX.Element = ({ popoverContent, expressionDescription, defaultValue, value, isInvalid }) => { const [popoverOpen, setPopoverOpen] = useState(false); @@ -61,7 +61,7 @@ export const ExpressionWithPopover: ({ iconType="cross" color="danger" aria-label={i18n.translate( - 'xpack.triggersActionsUI.sections.alertAdd.geoThreshold.closePopoverLabel', + 'xpack.stackAlerts.geoThreshold.ui.expressionPopover.closePopoverLabel', { defaultMessage: 'Close', } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/util_components/geo_index_pattern_select.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/geo_index_pattern_select.tsx similarity index 80% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/util_components/geo_index_pattern_select.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/geo_index_pattern_select.tsx index 42995dfb1b9d63..a552d6d998c7e3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/util_components/geo_index_pattern_select.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/geo_index_pattern_select.tsx @@ -14,6 +14,7 @@ import { HttpSetup } from 'kibana/public'; interface Props { onChange: (indexPattern: IndexPattern) => void; value: string | undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any IndexPatternSelectComponent: any; indexPatternService: IndexPatternsContract | undefined; http: HttpSetup; @@ -39,7 +40,7 @@ export class GeoIndexPatternSelect extends Component { this._isMounted = true; } - _onIndexPatternSelect = async (indexPatternId: any) => { + _onIndexPatternSelect = async (indexPatternId: string) => { if (!indexPatternId || indexPatternId.length === 0 || !this.props.indexPatternService) { return; } @@ -70,42 +71,39 @@ export class GeoIndexPatternSelect extends Component { return ( <>

@@ -123,7 +121,7 @@ export class GeoIndexPatternSelect extends Component { {this._renderNoIndexPatternWarning()} @@ -133,7 +131,7 @@ export class GeoIndexPatternSelect extends Component { indexPatternId={this.props.value} onChange={this._onIndexPatternSelect} placeholder={i18n.translate( - 'xpack.triggersActionsUI.geoThreshold.indexPatternSelectPlaceholder', + 'xpack.stackAlerts.geoThreshold.indexPatternSelectPlaceholder', { defaultMessage: 'Select index pattern', } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/util_components/single_field_select.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/single_field_select.tsx similarity index 96% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/util_components/single_field_select.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/single_field_select.tsx index 30389b31ce8c88..ef6e6f6f5e18fe 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/util_components/single_field_select.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/single_field_select.tsx @@ -14,7 +14,7 @@ import { EuiFlexItem, } from '@elastic/eui'; import { IFieldType } from 'src/plugins/data/public'; -import { FieldIcon } from '../../../../../../../../../../src/plugins/kibana_react/public'; +import { FieldIcon } from '../../../../../../../../src/plugins/kibana_react/public'; function fieldsToOptions(fields?: IFieldType[]): Array> { if (!fields) { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/types.ts b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/types.ts similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/types.ts rename to x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/types.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/validation.test.ts b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/validation.test.ts similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/validation.test.ts rename to x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/validation.test.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/validation.ts b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/validation.ts similarity index 72% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/validation.ts rename to x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/validation.ts index 078a88d9e8415f..7a511f681ecaa1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/validation.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/validation.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { ValidationResult } from '../../../../types'; +import { ValidationResult } from '../../../../triggers_actions_ui/public'; import { GeoThresholdAlertParams } from './types'; export const validateExpression = (alertParams: GeoThresholdAlertParams): ValidationResult => { @@ -35,7 +35,7 @@ export const validateExpression = (alertParams: GeoThresholdAlertParams): Valida if (!index) { errors.index.push( - i18n.translate('xpack.triggersActionsUI.geoThreshold.error.requiredIndexTitleText', { + i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredIndexTitleText', { defaultMessage: 'Index pattern is required.', }) ); @@ -43,7 +43,7 @@ export const validateExpression = (alertParams: GeoThresholdAlertParams): Valida if (!geoField) { errors.geoField.push( - i18n.translate('xpack.triggersActionsUI.geoThreshold.error.requiredGeoFieldText', { + i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredGeoFieldText', { defaultMessage: 'Geo field is required.', }) ); @@ -51,7 +51,7 @@ export const validateExpression = (alertParams: GeoThresholdAlertParams): Valida if (!entity) { errors.entity.push( - i18n.translate('xpack.triggersActionsUI.geoThreshold.error.requiredEntityText', { + i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredEntityText', { defaultMessage: 'Entity is required.', }) ); @@ -59,7 +59,7 @@ export const validateExpression = (alertParams: GeoThresholdAlertParams): Valida if (!dateField) { errors.dateField.push( - i18n.translate('xpack.triggersActionsUI.geoThreshold.error.requiredDateFieldText', { + i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredDateFieldText', { defaultMessage: 'Date field is required.', }) ); @@ -67,7 +67,7 @@ export const validateExpression = (alertParams: GeoThresholdAlertParams): Valida if (!trackingEvent) { errors.trackingEvent.push( - i18n.translate('xpack.triggersActionsUI.geoThreshold.error.requiredTrackingEventText', { + i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredTrackingEventText', { defaultMessage: 'Tracking event is required.', }) ); @@ -75,7 +75,7 @@ export const validateExpression = (alertParams: GeoThresholdAlertParams): Valida if (!boundaryType) { errors.boundaryType.push( - i18n.translate('xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryTypeText', { + i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredBoundaryTypeText', { defaultMessage: 'Boundary type is required.', }) ); @@ -83,7 +83,7 @@ export const validateExpression = (alertParams: GeoThresholdAlertParams): Valida if (!boundaryIndexTitle) { errors.boundaryIndexTitle.push( - i18n.translate('xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryIndexTitleText', { + i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredBoundaryIndexTitleText', { defaultMessage: 'Boundary index pattern title is required.', }) ); @@ -91,7 +91,7 @@ export const validateExpression = (alertParams: GeoThresholdAlertParams): Valida if (!boundaryGeoField) { errors.boundaryGeoField.push( - i18n.translate('xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryGeoFieldText', { + i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredBoundaryGeoFieldText', { defaultMessage: 'Boundary geo field is required.', }) ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/index.ts b/x-pack/plugins/stack_alerts/public/alert_types/index.ts similarity index 57% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/index.ts rename to x-pack/plugins/stack_alerts/public/alert_types/index.ts index 4b2860dcf9b721..61cf7193fedb73 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/index.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/index.ts @@ -6,18 +6,17 @@ import { getAlertType as getGeoThresholdAlertType } from './geo_threshold'; import { getAlertType as getThresholdAlertType } from './threshold'; -import { TypeRegistry } from '../../type_registry'; -import { AlertTypeModel } from '../../../types'; -import { TriggersActionsUiConfigType } from '../../../plugin'; +import { Config } from '../../common'; +import { TriggersAndActionsUIPublicPluginSetup } from '../../../triggers_actions_ui/public'; -export function registerBuiltInAlertTypes({ +export function registerAlertTypes({ alertTypeRegistry, - triggerActionsUiConfig, + config, }: { - alertTypeRegistry: TypeRegistry; - triggerActionsUiConfig: TriggersActionsUiConfigType; + alertTypeRegistry: TriggersAndActionsUIPublicPluginSetup['alertTypeRegistry']; + config: Config; }) { - if (triggerActionsUiConfig.enableGeoTrackingThresholdAlert) { + if (config.enableGeoTrackingThresholdAlert) { alertTypeRegistry.register(getGeoThresholdAlertType()); } alertTypeRegistry.register(getThresholdAlertType()); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.scss b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.scss similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.scss rename to x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.scss diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx similarity index 86% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx index e309d97b57f341..92cb8c9055bde4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx @@ -29,21 +29,20 @@ import { getIndexPatterns, getIndexOptions, getFields, -} from '../../../../common/index_controls'; -import { COMPARATORS, builtInComparators } from '../../../../common/constants'; -import { getTimeFieldOptions } from '../../../../common/lib/get_time_options'; -import { ThresholdVisualization } from './visualization'; -import { WhenExpression } from '../../../../common'; -import { + COMPARATORS, + builtInComparators, + getTimeFieldOptions, OfExpression, ThresholdExpression, ForLastExpression, GroupByExpression, -} from '../../../../common'; -import { builtInAggregationTypes } from '../../../../common/constants'; + WhenExpression, + builtInAggregationTypes, + AlertTypeParamsExpressionProps, + AlertsContextValue, +} from '../../../../triggers_actions_ui/public'; +import { ThresholdVisualization } from './visualization'; import { IndexThresholdAlertParams } from './types'; -import { AlertTypeParamsExpressionProps } from '../../../../types'; -import { AlertsContextValue } from '../../../context/alerts_context'; import './expression.scss'; const DEFAULT_VALUES = { @@ -89,7 +88,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent>([]); + const [esFields, setEsFields] = useState([]); const [indexOptions, setIndexOptions] = useState([]); const [timeFieldOptions, setTimeFieldOptions] = useState([firstFieldOption]); const [isIndiciesLoading, setIsIndiciesLoading] = useState(false); @@ -98,7 +97,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent expressionFieldsWithValidation.includes(errorKey) && errors[errorKey].length >= 1 && - (alertParams as { [key: string]: any })[errorKey] !== undefined + alertParams[errorKey as keyof IndexThresholdAlertParams] !== undefined ); const canShowVizualization = !!Object.keys(errors).find( @@ -106,7 +105,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent 0) { const currentEsFields = await getFields(http, index); - const timeFields = getTimeFieldOptions(currentEsFields as any); + const timeFields = getTimeFieldOptions(currentEsFields); setEsFields(currentEsFields); setTimeFieldOptions([firstFieldOption, ...timeFields]); @@ -159,7 +158,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent } @@ -167,7 +166,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent } @@ -211,7 +210,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent } @@ -284,7 +283,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent

@@ -296,12 +295,9 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent 0 ? renderIndices(index) : firstFieldOption.text} isActive={indexPopoverOpen} onClick={() => { @@ -321,12 +317,9 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent - {i18n.translate( - 'xpack.triggersActionsUI.sections.alertAdd.threshold.indexButtonLabel', - { - defaultMessage: 'index', - } - )} + {i18n.translate('xpack.stackAlerts.threshold.ui.alertParams.indexButtonLabel', { + defaultMessage: 'index', + })}
@@ -411,10 +404,10 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent + onChangeWindowSize={(selectedWindowSize: number | undefined) => setAlertParams('timeWindowSize', selectedWindowSize) } - onChangeWindowUnit={(selectedWindowUnit: any) => + onChangeWindowUnit={(selectedWindowUnit: string) => setAlertParams('timeWindowUnit', selectedWindowUnit) } /> @@ -427,7 +420,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/index.ts b/x-pack/plugins/stack_alerts/public/alert_types/threshold/index.ts similarity index 77% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/index.ts rename to x-pack/plugins/stack_alerts/public/alert_types/threshold/index.ts index a5b2fbb37e8381..b7923a30136131 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/index.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/index.ts @@ -5,18 +5,17 @@ */ import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; -import { AlertTypeModel } from '../../../../types'; import { validateExpression } from './validation'; import { IndexThresholdAlertParams } from './types'; -import { AlertsContextValue } from '../../../context/alerts_context'; +import { AlertTypeModel, AlertsContextValue } from '../../../../triggers_actions_ui/public'; export function getAlertType(): AlertTypeModel { return { id: '.index-threshold', - name: i18n.translate('xpack.triggersActionsUI.indexThresholdAlert.nameText', { + name: i18n.translate('xpack.stackAlerts.threshold.ui.alertType.nameText', { defaultMessage: 'Index threshold', }), - description: i18n.translate('xpack.triggersActionsUI.indexThresholdAlert.descriptionText', { + description: i18n.translate('xpack.stackAlerts.threshold.ui.alertType.descriptionText', { defaultMessage: 'Alert when an aggregated query meets the threshold.', }), iconClass: 'alert', @@ -26,7 +25,7 @@ export function getAlertType(): AlertTypeModel import('./expression')), validate: validateExpression, defaultActionMessage: i18n.translate( - 'xpack.triggersActionsUI.components.builtinAlertTypes.threshold.alertDefaultActionMessage', + 'xpack.stackAlerts.threshold.ui.alertType.defaultActionMessage', { defaultMessage: `alert \\{\\{alertName\\}\\} group \\{\\{context.group\\}\\} value \\{\\{context.value\\}\\} exceeded threshold \\{\\{context.function\\}\\} over \\{\\{params.timeWindowSize\\}\\}\\{\\{params.timeWindowUnit\\}\\} on \\{\\{context.date\\}\\}`, } diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/index_threshold_api.ts b/x-pack/plugins/stack_alerts/public/alert_types/threshold/index_threshold_api.ts new file mode 100644 index 00000000000000..ec531b26fc8c66 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/index_threshold_api.ts @@ -0,0 +1,45 @@ +/* + * 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 { HttpSetup } from 'kibana/public'; +import { TimeSeriesResult } from '../../../../triggers_actions_ui/common'; +import { IndexThresholdAlertParams } from './types'; + +const INDEX_THRESHOLD_DATA_API_ROOT = '/api/triggers_actions_ui/data'; + +export interface GetThresholdAlertVisualizationDataParams { + model: IndexThresholdAlertParams; + visualizeOptions: { + rangeFrom: string; + rangeTo: string; + interval: string; + }; + http: HttpSetup; +} + +export async function getThresholdAlertVisualizationData({ + model, + visualizeOptions, + http, +}: GetThresholdAlertVisualizationDataParams): Promise { + const timeSeriesQueryParams = { + index: model.index, + timeField: model.timeField, + aggType: model.aggType, + aggField: model.aggField, + groupBy: model.groupBy, + termField: model.termField, + termSize: model.termSize, + timeWindowSize: model.timeWindowSize, + timeWindowUnit: model.timeWindowUnit, + dateStart: new Date(visualizeOptions.rangeFrom).toISOString(), + dateEnd: new Date(visualizeOptions.rangeTo).toISOString(), + interval: visualizeOptions.interval, + }; + + return await http.post(`${INDEX_THRESHOLD_DATA_API_ROOT}/_time_series_query`, { + body: JSON.stringify(timeSeriesQueryParams), + }); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/types.ts b/x-pack/plugins/stack_alerts/public/alert_types/threshold/types.ts similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/types.ts rename to x-pack/plugins/stack_alerts/public/alert_types/threshold/types.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/validation.test.ts b/x-pack/plugins/stack_alerts/public/alert_types/threshold/validation.test.ts similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/validation.test.ts rename to x-pack/plugins/stack_alerts/public/alert_types/threshold/validation.test.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/validation.ts b/x-pack/plugins/stack_alerts/public/alert_types/threshold/validation.ts similarity index 81% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/validation.ts rename to x-pack/plugins/stack_alerts/public/alert_types/threshold/validation.ts index 3912b2fffae1eb..4bbf80906377be 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/validation.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/validation.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { ValidationResult } from '../../../../types'; import { IndexThresholdAlertParams } from './types'; import { + ValidationResult, builtInGroupByTypes, builtInAggregationTypes, builtInComparators, -} from '../../../../common/constants'; +} from '../../../../triggers_actions_ui/public'; export const validateExpression = (alertParams: IndexThresholdAlertParams): ValidationResult => { const { @@ -39,21 +39,21 @@ export const validateExpression = (alertParams: IndexThresholdAlertParams): Vali validationResult.errors = errors; if (!index || index.length === 0) { errors.index.push( - i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredIndexText', { + i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.requiredIndexText', { defaultMessage: 'Index is required.', }) ); } if (!timeField) { errors.timeField.push( - i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredTimeFieldText', { + i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.requiredTimeFieldText', { defaultMessage: 'Time field is required.', }) ); } if (aggType && builtInAggregationTypes[aggType].fieldRequired && !aggField) { errors.aggField.push( - i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredAggFieldText', { + i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.requiredAggFieldText', { defaultMessage: 'Aggregation field is required.', }) ); @@ -65,7 +65,7 @@ export const validateExpression = (alertParams: IndexThresholdAlertParams): Vali !termSize ) { errors.termSize.push( - i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredTermSizedText', { + i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.requiredTermSizedText', { defaultMessage: 'Term size is required.', }) ); @@ -77,21 +77,21 @@ export const validateExpression = (alertParams: IndexThresholdAlertParams): Vali !termField ) { errors.termField.push( - i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredtTermFieldText', { + i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.requiredTermFieldText', { defaultMessage: 'Term field is required.', }) ); } if (!timeWindowSize) { errors.timeWindowSize.push( - i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredTimeWindowSizeText', { + i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.requiredTimeWindowSizeText', { defaultMessage: 'Time window size is required.', }) ); } if (!threshold || threshold.length === 0 || threshold[0] === undefined) { errors.threshold0.push( - i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredThreshold0Text', { + i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold0Text', { defaultMessage: 'Threshold0 is required.', }) ); @@ -104,14 +104,14 @@ export const validateExpression = (alertParams: IndexThresholdAlertParams): Vali (threshold && threshold.length < builtInComparators[thresholdComparator!].requiredValues)) ) { errors.threshold1.push( - i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredThreshold1Text', { + i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold1Text', { defaultMessage: 'Threshold1 is required.', }) ); } if (threshold && threshold.length === 2 && threshold[0] > threshold[1]) { errors.threshold1.push( - i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.greaterThenThreshold0Text', { + i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.greaterThenThreshold0Text', { defaultMessage: 'Threshold1 should be > Threshold0.', }) ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx b/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx similarity index 86% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx index a282fa08e8f38c..6145aa3671a7f7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx @@ -29,11 +29,17 @@ import { EuiLoadingSpinner, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { getThresholdAlertVisualizationData } from '../../../../common/lib/index_threshold_api'; -import { AggregationType, Comparator } from '../../../../common/types'; -import { AlertsContextValue } from '../../../context/alerts_context'; +import { + getThresholdAlertVisualizationData, + GetThresholdAlertVisualizationDataParams, +} from './index_threshold_api'; +import { + AlertsContextValue, + AggregationType, + Comparator, +} from '../../../../triggers_actions_ui/public'; import { IndexThresholdAlertParams } from './types'; -import { parseDuration } from '../../../../../../alerts/common/parse_duration'; +import { parseDuration } from '../../../../alerts/common/parse_duration'; const customTheme = () => { return { @@ -125,7 +131,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ const { http, toastNotifications, charts, uiSettings, dataFieldsFormats } = alertsContext; const [loadingState, setLoadingState] = useState(null); - const [error, setError] = useState(undefined); + const [error, setError] = useState(undefined); const [visualizationData, setVisualizationData] = useState>(); const [startVisualizationAt, setStartVisualizationAt] = useState(new Date()); @@ -150,7 +156,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ if (toastNotifications) { toastNotifications.addDanger({ title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertAdd.unableToLoadVisualizationMessage', + 'xpack.stackAlerts.threshold.ui.visualization.unableToLoadVisualizationMessage', { defaultMessage: 'Unable to load visualization' } ), }); @@ -199,7 +205,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ body={ @@ -215,7 +221,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ @@ -239,7 +245,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ const alertVisualizationDataKeys = Object.keys(visualizationData); const timezone = getTimezone(uiSettings); const actualThreshold = getThreshold(); - let maxY = actualThreshold[actualThreshold.length - 1] as any; + let maxY = actualThreshold[actualThreshold.length - 1]; (Object.values(visualizationData) as number[][][]).forEach((data) => { data.forEach(([, y]) => { @@ -288,14 +294,14 @@ export const ThresholdVisualization: React.FunctionComponent = ({ /> ); })} - {actualThreshold.map((_value: any, i: number) => { - const specId = i === 0 ? 'threshold' : `threshold${i}`; + {actualThreshold.map((_value: number, thresholdIndex: number) => { + const specId = thresholdIndex === 0 ? 'threshold' : `threshold${thresholdIndex}`; return ( ); })} @@ -305,14 +311,14 @@ export const ThresholdVisualization: React.FunctionComponent = ({ size="s" title={ } color="warning" > @@ -325,7 +331,11 @@ export const ThresholdVisualization: React.FunctionComponent = ({ }; // convert the data from the visualization API into something easier to digest with charts -async function getVisualizationData(model: any, visualizeOptions: any, http: HttpSetup) { +async function getVisualizationData( + model: IndexThresholdAlertParams, + visualizeOptions: GetThresholdAlertVisualizationDataParams['visualizeOptions'], + http: HttpSetup +) { const vizData = await getThresholdAlertVisualizationData({ model, visualizeOptions, diff --git a/x-pack/plugins/stack_alerts/public/index.ts b/x-pack/plugins/stack_alerts/public/index.ts new file mode 100644 index 00000000000000..2f84a5949f1117 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { PluginInitializerContext } from 'src/core/public'; +import { StackAlertsPublicPlugin } from './plugin'; + +export const plugin = (ctx: PluginInitializerContext) => new StackAlertsPublicPlugin(ctx); diff --git a/x-pack/plugins/stack_alerts/public/plugin.tsx b/x-pack/plugins/stack_alerts/public/plugin.tsx new file mode 100644 index 00000000000000..63176e7b302770 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/plugin.tsx @@ -0,0 +1,35 @@ +/* + * 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 { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/public'; +import { TriggersAndActionsUIPublicPluginSetup } from '../../../plugins/triggers_actions_ui/public'; +import { registerAlertTypes } from './alert_types'; +import { Config } from '../common'; + +export type Setup = void; +export type Start = void; + +export interface StackAlertsPublicSetupDeps { + triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; +} + +export class StackAlertsPublicPlugin implements Plugin { + private initializerContext: PluginInitializerContext; + + constructor(initializerContext: PluginInitializerContext) { + this.initializerContext = initializerContext; + } + + public setup(core: CoreSetup, { triggersActionsUi }: StackAlertsPublicSetupDeps) { + registerAlertTypes({ + alertTypeRegistry: triggersActionsUi.alertTypeRegistry, + config: this.initializerContext.config.get(), + }); + } + + public start() {} + public stop() {} +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts index 360a6eb1695730..9fc46fe2f2586f 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; -import { Service } from '../../types'; +import { Logger } from 'src/core/server'; import { STACK_ALERTS_FEATURE_ID } from '../../../common'; import { getGeoThresholdExecutor } from './geo_threshold'; import { @@ -173,7 +173,7 @@ export interface GeoThresholdParams { } export function getAlertType( - service: Omit + logger: Logger ): { defaultActionGroupId: string; actionGroups: ActionGroup[]; @@ -222,7 +222,7 @@ export function getAlertType( name: alertTypeName, actionGroups: [{ id: ActionGroupId, name: actionGroupName }], defaultActionGroupId: ActionGroupId, - executor: getGeoThresholdExecutor(service), + executor: getGeoThresholdExecutor(logger), producer: STACK_ALERTS_FEATURE_ID, validate: { params: ParamsSchema, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/es_query_builder.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/es_query_builder.ts index c4238e62ff2614..97be51b2a62562 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/es_query_builder.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/es_query_builder.ts @@ -6,7 +6,7 @@ import { ILegacyScopedClusterClient } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; -import { Logger } from '../../types'; +import { Logger } from 'src/core/server'; export const OTHER_CATEGORY = 'other'; // Consider dynamically obtaining from config? diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts index f30dea151ece82..394ee8d606abe8 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts @@ -6,10 +6,10 @@ import _ from 'lodash'; import { SearchResponse } from 'elasticsearch'; +import { Logger } from 'src/core/server'; import { executeEsQueryFactory, getShapesFilters, OTHER_CATEGORY } from './es_query_builder'; import { AlertServices, AlertTypeState } from '../../../../alerts/server'; import { ActionGroupId, GEO_THRESHOLD_ID, GeoThresholdParams } from './alert_type'; -import { Logger } from '../../types'; interface LatestEntityLocation { location: number[]; @@ -169,7 +169,7 @@ function getOffsetTime(delayOffsetWithUnits: string, oldTime: Date): Date { return adjustedDate; } -export const getGeoThresholdExecutor = ({ logger: log }: { logger: Logger }) => +export const getGeoThresholdExecutor = (log: Logger) => async function ({ previousStartedAt, startedAt, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/index.ts index d57f219bb8f9a8..2fa2bed9d8419d 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/index.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/index.ts @@ -4,15 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Service, AlertingSetup } from '../../types'; +import { Logger } from 'src/core/server'; +import { AlertingSetup } from '../../types'; import { getAlertType } from './alert_type'; interface RegisterParams { - service: Omit; + logger: Logger; alerts: AlertingSetup; } export function register(params: RegisterParams) { - const { service, alerts } = params; - alerts.registerType(getAlertType(service)); + const { logger, alerts } = params; + alerts.registerType(getAlertType(logger)); } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/alert_type.test.ts index 5cf113f519a5a5..49b56b5571b441 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/alert_type.test.ts @@ -8,11 +8,9 @@ import { loggingSystemMock } from '../../../../../../../src/core/server/mocks'; import { getAlertType, GeoThresholdParams } from '../alert_type'; describe('alertType', () => { - const service = { - logger: loggingSystemMock.create().get(), - }; + const logger = loggingSystemMock.create().get(); - const alertType = getAlertType(service); + const alertType = getAlertType(logger); it('alert type creation structure is the expected value', async () => { expect(alertType.id).toBe('.geo-threshold'); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/index.ts index dd9f1488092f43..461358d1296e23 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index.ts @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Service, IRouter, AlertingSetup } from '../types'; +import { Logger } from 'src/core/server'; +import { AlertingSetup, StackAlertsStartDeps } from '../types'; import { register as registerIndexThreshold } from './index_threshold'; import { register as registerGeoThreshold } from './geo_threshold'; interface RegisterAlertTypesParams { - service: Service; - router: IRouter; + logger: Logger; + data: Promise; alerts: AlertingSetup; - baseRoute: string; } export function registerBuiltInAlertTypes(params: RegisterAlertTypesParams) { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/README.md b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/README.md index 0ff01ddfb49c7b..9b0eb23950cc3a 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/README.md +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/README.md @@ -13,10 +13,7 @@ is exceeded. ## alertType `.index-threshold` -The alertType parameters are specified in -[`lib/core_query_types.ts`][it-core-query] -and -[`alert_type_params.ts`][it-alert-params]. +The alertType parameters are specified in [`alert_type_params.ts`][it-alert-params]. The alertType has a single actionGroup, `'threshold met'`. The `context` object provided to actions is specified in @@ -123,227 +120,6 @@ server log [17:32:10.060] [warning][actions][actions][plugins] \ [now-iso]: https://github.com/pmuellr/now-iso -## http endpoints +## Data Apis via the TriggersActionsUi plugin and its http endpoints -The following endpoints are provided for this alert type: - -- `POST /api/stack_alerts/index_threshold/_indices` -- `POST /api/stack_alerts/index_threshold/_fields` -- `POST /api/stack_alerts/index_threshold/_time_series_query` - -### `POST .../_indices` - -This HTTP endpoint is provided for the alerting ui to list the available -"index names" for the user to select to use with the alert. This API also -returns aliases which match the supplied pattern. - -The request body is expected to be a JSON object in the following form, where the -`pattern` value may include comma-separated names and wildcards. - -```js -{ - pattern: "index-name-pattern" -} -``` - -The response body is a JSON object in the following form, where each element -of the `indices` array is the name of an index or alias. The number of elements -returned is limited, as this API is intended to be used to help narrow down -index names to use with the alert, and not support pagination, etc. - -```js -{ - indices: ["index-name-1", "alias-name-1", ...] -} -``` - -### `POST .../_fields` - -This HTTP endpoint is provided for the alerting ui to list the available -fields for the user to select to use with the alert. - -The request body is expected to be a JSON object in the following form, where the -`indexPatterns` array elements may include comma-separated names and wildcards. - -```js -{ - indexPatterns: ["index-pattern-1", "index-pattern-2"] -} -``` - -The response body is a JSON object in the following form, where each element -fields array is a field object. - -```js -{ - fields: [fieldObject1, fieldObject2, ...] -} -``` - -A field object is the following shape: - -```typescript -{ - name: string, // field name - type: string, // field type - eg 'keyword', 'date', 'long', etc - normalizedType: string, // for numeric types, this will be 'number' - aggregatable: true, // value from elasticsearch field capabilities - searchable: true, // value from elasticsearch field capabilities -} -``` - -### `POST .../_time_series_query` - -This HTTP endpoint is provided to return the values the alertType would calculate, -over a series of time. It is intended to be used in the alerting UI to -provide a "preview" of the alert during creation/editing based on recent data, -and could be used to show a "simulation" of the the alert over an arbitrary -range of time. - -The endpoint is `POST /api/stack_alerts/index_threshold/_time_series_query`. -The request and response bodies are specifed in -[`lib/core_query_types.ts`][it-core-query] -and -[`lib/time_series_types.ts`][it-timeSeries-types]. -The request body is very similar to the alertType's parameters. - -### example - -Continuing with the example above, here's a query to get the values calculated -for the last 10 seconds. -This example uses [now-iso][] to generate iso date strings. - -```console -curl -k "https://elastic:changeme@localhost:5601/api/stack_alerts/index_threshold/_time_series_query" \ - -H "kbn-xsrf: foo" -H "content-type: application/json" -d "{ - \"index\": \"es-hb-sim\", - \"timeField\": \"@timestamp\", - \"aggType\": \"avg\", - \"aggField\": \"summary.up\", - \"groupBy\": \"top\", - \"termSize\": 100, - \"termField\": \"monitor.name.keyword\", - \"interval\": \"1s\", - \"dateStart\": \"`now-iso -10s`\", - \"dateEnd\": \"`now-iso`\", - \"timeWindowSize\": 5, - \"timeWindowUnit\": \"s\" -}" -``` - -``` -{ - "results": [ - { - "group": "host-A", - "metrics": [ - [ "2020-02-26T15:10:40.000Z", 0 ], - [ "2020-02-26T15:10:41.000Z", 0 ], - [ "2020-02-26T15:10:42.000Z", 0 ], - [ "2020-02-26T15:10:43.000Z", 0 ], - [ "2020-02-26T15:10:44.000Z", 0 ], - [ "2020-02-26T15:10:45.000Z", 0 ], - [ "2020-02-26T15:10:46.000Z", 0 ], - [ "2020-02-26T15:10:47.000Z", 0 ], - [ "2020-02-26T15:10:48.000Z", 0 ], - [ "2020-02-26T15:10:49.000Z", 0 ], - [ "2020-02-26T15:10:50.000Z", 0 ] - ] - } - ] -} -``` - -To get the current value of the calculated metric, you can leave off the date: - -``` -curl -k "https://elastic:changeme@localhost:5601/api/stack_alerts/index_threshold/_time_series_query" \ - -H "kbn-xsrf: foo" -H "content-type: application/json" -d '{ - "index": "es-hb-sim", - "timeField": "@timestamp", - "aggType": "avg", - "aggField": "summary.up", - "groupBy": "top", - "termField": "monitor.name.keyword", - "termSize": 100, - "interval": "1s", - "timeWindowSize": 5, - "timeWindowUnit": "s" -}' -``` - -``` -{ - "results": [ - { - "group": "host-A", - "metrics": [ - [ "2020-02-26T15:23:36.635Z", 0 ] - ] - } - ] -} -``` - -[it-timeSeries-types]: lib/time_series_types.ts - -## service functions - -A single service function is available that provides the functionality -of the http endpoint `POST /api/stack_alerts/index_threshold/_time_series_query`, -but as an API for Kibana plugins. The function is available as -`alertingService.indexThreshold.timeSeriesQuery()` - -The parameters and return value for the function are the same as for the HTTP -request, though some additional parameters are required (logger, callCluster, -etc). - -## notes on the timeSeriesQuery API / http endpoint - -This API provides additional parameters beyond what the alertType itself uses: - -- `dateStart` -- `dateEnd` -- `interval` - -The `dateStart` and `dateEnd` parameters are ISO date strings. - -The `interval` parameter is intended to model the `interval` the alert is -currently using, and uses the same `1s`, `2m`, `3h`, etc format. Over the -supplied date range, a time-series data point will be calculated every -`interval` duration. - -So the number of time-series points in the output of the API should be: - -``` -( dateStart - dateEnd ) / interval -``` - -Example: - -``` -dateStart: '2020-01-01T00:00:00' -dateEnd: '2020-01-02T00:00:00' -interval: '1h' -``` - -The date range is 1 day === 24 hours. The interval is 1 hour. So there should -be ~24 time series points in the output. - -For preview purposes: - -- The `termSize` parameter should be used to help cut -down on the amount of work ES does, and keep the generated graphs a little -simpler. Probably something like `10`. - -- For queries with long date ranges, you probably don't want to use the -`interval` the alert is set to, as the `interval` used in the query, as this -could result in a lot of time-series points being generated, which is both -costly in ES, and may result in noisy graphs. - -- The `timeWindow*` parameters should be the same as what the alert is using, -especially for the `count` and `sum` aggregation types. Those aggregations -don't scale the same way the others do, when the window changes. Even for -the other aggregations, changing the window could result in dramatically -different values being generated - `avg` will be more "average-y", `min` -and `max` will be a little stickier. \ No newline at end of file +The Index Threshold Alert Type is backed by Apis exposed by the [TriggersActionsUi plugin](../../../../triggers_actions_ui/README.md). diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts index d75f3af22ab06a..0febe805af4e09 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts @@ -9,14 +9,12 @@ import { getAlertType } from './alert_type'; import { Params } from './alert_type_params'; describe('alertType', () => { - const service = { - indexThreshold: { - timeSeriesQuery: jest.fn(), - }, - logger: loggingSystemMock.create().get(), + const logger = loggingSystemMock.create().get(); + const data = { + timeSeriesQuery: jest.fn(), }; - const alertType = getAlertType(service); + const alertType = getAlertType(logger, Promise.resolve(data)); it('alert type creation structure is the expected value', async () => { expect(alertType.id).toBe('.index-threshold'); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts index e0a9cd981dac09..2d9e1b3adc1b89 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts @@ -5,23 +5,26 @@ */ import { i18n } from '@kbn/i18n'; -import { AlertType, AlertExecutorOptions } from '../../types'; +import { Logger } from 'src/core/server'; +import { AlertType, AlertExecutorOptions, StackAlertsStartDeps } from '../../types'; import { Params, ParamsSchema } from './alert_type_params'; import { ActionContext, BaseActionContext, addMessages } from './action_context'; -import { TimeSeriesQuery } from './lib/time_series_query'; -import { Service } from '../../types'; import { STACK_ALERTS_FEATURE_ID } from '../../../common'; +import { + CoreQueryParamsSchemaProperties, + TimeSeriesQuery, +} from '../../../../triggers_actions_ui/server'; export const ID = '.index-threshold'; -import { CoreQueryParamsSchemaProperties } from './lib/core_query_types'; const ActionGroupId = 'threshold met'; const ComparatorFns = getComparatorFns(); export const ComparatorFnNames = new Set(ComparatorFns.keys()); -export function getAlertType(service: Service): AlertType { - const { logger } = service; - +export function getAlertType( + logger: Logger, + data: Promise +): AlertType { const alertTypeName = i18n.translate('xpack.stackAlerts.indexThreshold.alertTypeTitle', { defaultMessage: 'Index threshold', }); @@ -152,7 +155,7 @@ export function getAlertType(service: Service): AlertType> = { index: 'index-name', @@ -71,3 +71,185 @@ describe('alertType Params validate()', () => { return ParamsSchema.validate(params); } }); + +export function runTests(schema: ObjectType, defaultTypeParams: Record): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let params: any; + + const CoreDefaultParams: Writable> = { + index: 'index-name', + timeField: 'time-field', + aggType: 'count', + groupBy: 'all', + timeWindowSize: 5, + timeWindowUnit: 'm', + }; + + describe('coreQueryTypes', () => { + beforeEach(() => { + params = { ...CoreDefaultParams, ...defaultTypeParams }; + }); + + it('succeeds with minimal properties', async () => { + expect(validate()).toBeTruthy(); + }); + + it('succeeds with maximal properties', async () => { + params.aggType = 'avg'; + params.aggField = 'agg-field'; + params.groupBy = 'top'; + params.termField = 'group-field'; + params.termSize = 200; + expect(validate()).toBeTruthy(); + + params.index = ['index-name-1', 'index-name-2']; + params.aggType = 'avg'; + params.aggField = 'agg-field'; + params.groupBy = 'top'; + params.termField = 'group-field'; + params.termSize = 200; + expect(validate()).toBeTruthy(); + }); + + it('fails for invalid index', async () => { + delete params.index; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[index]: expected at least one defined value but got [undefined]"` + ); + + params.index = 42; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot(` +"[index]: types that failed validation: +- [index.0]: expected value of type [string] but got [number] +- [index.1]: expected value of type [array] but got [number]" +`); + + params.index = ''; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot(` +"[index]: types that failed validation: +- [index.0]: value has length [0] but it must have a minimum length of [1]. +- [index.1]: could not parse array value from json input" +`); + + params.index = ['', 'a']; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot(` +"[index]: types that failed validation: +- [index.0]: expected value of type [string] but got [Array] +- [index.1.0]: value has length [0] but it must have a minimum length of [1]." +`); + }); + + it('fails for invalid timeField', async () => { + delete params.timeField; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[timeField]: expected value of type [string] but got [undefined]"` + ); + + params.timeField = 42; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[timeField]: expected value of type [string] but got [number]"` + ); + + params.timeField = ''; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[timeField]: value has length [0] but it must have a minimum length of [1]."` + ); + }); + + it('fails for invalid aggType', async () => { + params.aggType = 42; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[aggType]: expected value of type [string] but got [number]"` + ); + + params.aggType = '-not-a-valid-aggType-'; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[aggType]: invalid aggType: \\"-not-a-valid-aggType-\\""` + ); + }); + + it('fails for invalid aggField', async () => { + params.aggField = 42; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[aggField]: expected value of type [string] but got [number]"` + ); + + params.aggField = ''; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[aggField]: value has length [0] but it must have a minimum length of [1]."` + ); + }); + + it('fails for invalid termField', async () => { + params.groupBy = 'top'; + params.termField = 42; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[termField]: expected value of type [string] but got [number]"` + ); + + params.termField = ''; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[termField]: value has length [0] but it must have a minimum length of [1]."` + ); + }); + + it('fails for invalid termSize', async () => { + params.groupBy = 'top'; + params.termField = 'fee'; + params.termSize = 'foo'; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[termSize]: expected value of type [number] but got [string]"` + ); + + params.termSize = MAX_GROUPS + 1; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[termSize]: must be less than or equal to 1000"` + ); + + params.termSize = 0; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[termSize]: Value must be equal to or greater than [1]."` + ); + }); + + it('fails for invalid timeWindowSize', async () => { + params.timeWindowSize = 'foo'; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[timeWindowSize]: expected value of type [number] but got [string]"` + ); + + params.timeWindowSize = 0; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[timeWindowSize]: Value must be equal to or greater than [1]."` + ); + }); + + it('fails for invalid timeWindowUnit', async () => { + params.timeWindowUnit = 42; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[timeWindowUnit]: expected value of type [string] but got [number]"` + ); + + params.timeWindowUnit = 'x'; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[timeWindowUnit]: invalid timeWindowUnit: \\"x\\""` + ); + }); + + it('fails for invalid aggType/aggField', async () => { + params.aggType = 'avg'; + delete params.aggField; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[aggField]: must have a value when [aggType] is \\"avg\\""` + ); + }); + }); + + function onValidate(): () => void { + return () => validate(); + } + + function validate(): unknown { + return schema.validate(params); + } +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts index 34d74fa98f9591..b51545770dd7bf 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts @@ -7,7 +7,10 @@ import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import { ComparatorFnNames, getInvalidComparatorMessage } from './alert_type'; -import { CoreQueryParamsSchemaProperties, validateCoreQueryBody } from './lib/core_query_types'; +import { + CoreQueryParamsSchemaProperties, + validateCoreQueryBody, +} from '../../../../triggers_actions_ui/server'; // alert type parameters diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/index.ts index 9787ece323c593..a075b0d614cbbb 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/index.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/index.ts @@ -4,34 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Service, AlertingSetup, IRouter } from '../../types'; -import { timeSeriesQuery } from './lib/time_series_query'; +import { Logger } from 'src/core/server'; +import { AlertingSetup, StackAlertsStartDeps } from '../../types'; import { getAlertType } from './alert_type'; -import { registerRoutes } from './routes'; // future enhancement: make these configurable? export const MAX_INTERVALS = 1000; export const MAX_GROUPS = 1000; export const DEFAULT_GROUPS = 100; -export function getService() { - return { - timeSeriesQuery, - }; -} - interface RegisterParams { - service: Service; - router: IRouter; + logger: Logger; + data: Promise; alerts: AlertingSetup; - baseRoute: string; } export function register(params: RegisterParams) { - const { service, router, alerts, baseRoute } = params; - - alerts.registerType(getAlertType(service)); - - const baseBuiltInRoute = `${baseRoute}/index_threshold`; - registerRoutes({ service, router, baseRoute: baseBuiltInRoute }); + const { logger, data, alerts } = params; + alerts.registerType(getAlertType(logger, data)); } diff --git a/x-pack/plugins/stack_alerts/server/index.ts b/x-pack/plugins/stack_alerts/server/index.ts index 108393c0d1469b..adb617558e6f46 100644 --- a/x-pack/plugins/stack_alerts/server/index.ts +++ b/x-pack/plugins/stack_alerts/server/index.ts @@ -4,15 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'src/core/server'; +import { PluginConfigDescriptor, PluginInitializerContext } from 'src/core/server'; import { AlertingBuiltinsPlugin } from './plugin'; -import { configSchema } from './config'; +import { configSchema, Config } from '../common/config'; export { ID as INDEX_THRESHOLD_ID } from './alert_types/index_threshold/alert_type'; -export const plugin = (ctx: PluginInitializerContext) => new AlertingBuiltinsPlugin(ctx); - -export const config = { +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + enableGeoTrackingThresholdAlert: true, + }, schema: configSchema, + deprecations: ({ renameFromRoot }) => [ + renameFromRoot( + 'xpack.triggers_actions_ui.enableGeoTrackingThresholdAlert', + 'xpack.stack_alerts.enableGeoTrackingThresholdAlert' + ), + ], }; -export { IService } from './types'; +export const plugin = (ctx: PluginInitializerContext) => new AlertingBuiltinsPlugin(ctx); diff --git a/x-pack/plugins/stack_alerts/server/plugin.test.ts b/x-pack/plugins/stack_alerts/server/plugin.test.ts index 3e2a919be0f13f..71972707852fec 100644 --- a/x-pack/plugins/stack_alerts/server/plugin.test.ts +++ b/x-pack/plugins/stack_alerts/server/plugin.test.ts @@ -69,34 +69,5 @@ describe('AlertingBuiltins Plugin', () => { expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledWith(BUILT_IN_ALERTS_FEATURE); }); - - it('should return a service in the expected shape', async () => { - const alertingSetup = alertsMock.createSetup(); - const featuresSetup = featuresPluginMock.createSetup(); - const service = await plugin.setup(coreSetup, { - alerts: alertingSetup, - features: featuresSetup, - }); - - expect(typeof service.indexThreshold.timeSeriesQuery).toBe('function'); - }); - }); - - describe('start()', () => { - let context: ReturnType; - let plugin: AlertingBuiltinsPlugin; - let coreStart: ReturnType; - - beforeEach(() => { - context = coreMock.createPluginInitializerContext(); - plugin = new AlertingBuiltinsPlugin(context); - coreStart = coreMock.createStart(); - }); - - it('should return a service in the expected shape', async () => { - const service = await plugin.start(coreStart); - - expect(typeof service.indexThreshold.timeSeriesQuery).toBe('function'); - }); }); }); diff --git a/x-pack/plugins/stack_alerts/server/plugin.ts b/x-pack/plugins/stack_alerts/server/plugin.ts index f250bbc70fb803..66ac9e455e8b6a 100644 --- a/x-pack/plugins/stack_alerts/server/plugin.ts +++ b/x-pack/plugins/stack_alerts/server/plugin.ts @@ -4,40 +4,35 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Plugin, Logger, CoreSetup, CoreStart, PluginInitializerContext } from 'src/core/server'; +import { Plugin, Logger, CoreSetup, PluginInitializerContext } from 'src/core/server'; -import { Service, IService, StackAlertsDeps } from './types'; -import { getService as getServiceIndexThreshold } from './alert_types/index_threshold'; +import { StackAlertsDeps, StackAlertsStartDeps } from './types'; import { registerBuiltInAlertTypes } from './alert_types'; import { BUILT_IN_ALERTS_FEATURE } from './feature'; -export class AlertingBuiltinsPlugin implements Plugin { +export class AlertingBuiltinsPlugin + implements Plugin { private readonly logger: Logger; - private readonly service: Service; constructor(ctx: PluginInitializerContext) { this.logger = ctx.logger.get(); - this.service = { - indexThreshold: getServiceIndexThreshold(), - logger: this.logger, - }; } - public async setup(core: CoreSetup, { alerts, features }: StackAlertsDeps): Promise { + public async setup( + core: CoreSetup, + { alerts, features }: StackAlertsDeps + ): Promise { features.registerKibanaFeature(BUILT_IN_ALERTS_FEATURE); registerBuiltInAlertTypes({ - service: this.service, - router: core.http.createRouter(), + logger: this.logger, + data: core + .getStartServices() + .then(async ([, { triggersActionsUi }]) => triggersActionsUi.data), alerts, - baseRoute: '/api/stack_alerts', }); - return this.service; - } - - public async start(core: CoreStart): Promise { - return this.service; } + public async start(): Promise {} public async stop(): Promise {} } diff --git a/x-pack/plugins/stack_alerts/server/types.ts b/x-pack/plugins/stack_alerts/server/types.ts index d0eb8aa768915e..e37596e8ff9700 100644 --- a/x-pack/plugins/stack_alerts/server/types.ts +++ b/x-pack/plugins/stack_alerts/server/types.ts @@ -4,11 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Logger, LegacyScopedClusterClient } from '../../../../src/core/server'; +import { PluginStartContract as TriggersActionsUiStartContract } from '../../triggers_actions_ui/server'; import { PluginSetupContract as AlertingSetup } from '../../alerts/server'; -import { getService as getServiceIndexThreshold } from './alert_types/index_threshold'; - -export { Logger, IRouter } from '../../../../src/core/server'; export { PluginSetupContract as AlertingSetup, @@ -23,14 +20,6 @@ export interface StackAlertsDeps { features: FeaturesPluginSetup; } -// external service exposed through plugin setup/start -export interface IService { - indexThreshold: ReturnType; -} - -// version of service for internal use -export interface Service extends IService { - logger: Logger; +export interface StackAlertsStartDeps { + triggersActionsUi: TriggersActionsUiStartContract; } - -export type CallCluster = LegacyScopedClusterClient['callAsCurrentUser']; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 912cb01d458e2e..238b3dccc698a4 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -19525,24 +19525,24 @@ "xpack.stackAlerts.indexThreshold.actionVariableContextThresholdLabel": "しきい値として使用する値の配列。「between」と「notBetween」には2つの値が必要です。その他は1つの値が必要です。", "xpack.stackAlerts.indexThreshold.actionVariableContextTitleLabel": "アラートの事前構成タイトル。", "xpack.stackAlerts.indexThreshold.actionVariableContextValueLabel": "しきい値を超えた値。", - "xpack.stackAlerts.indexThreshold.aggTypeRequiredErrorMessage": "[aggType]が「{aggType}」のときには[aggField]に値が必要です", + "xpack.triggersActionsUI.data.coreQueryParams.aggTypeRequiredErrorMessage": "[aggType]が「{aggType}」のときには[aggField]に値が必要です", "xpack.stackAlerts.indexThreshold.alertTypeContextMessageDescription": "アラート{name}グループ{group}値{value}が{date}に{window}にわたってしきい値{function}を超えました", "xpack.stackAlerts.indexThreshold.alertTypeContextSubjectTitle": "アラート{name}グループ{group}がしきい値を超えました", "xpack.stackAlerts.indexThreshold.alertTypeTitle": "インデックスしきい値", - "xpack.stackAlerts.indexThreshold.dateStartGTdateEndErrorMessage": "[dateStart]が[dateEnd]よりも大です", - "xpack.stackAlerts.indexThreshold.formattedFieldErrorMessage": "{fieldName}の無効な{formatName}形式:「{fieldValue}」", - "xpack.stackAlerts.indexThreshold.intervalRequiredErrorMessage": "[interval]: [dateStart]が[dateEnd]と等しくない場合に指定する必要があります", - "xpack.stackAlerts.indexThreshold.invalidAggTypeErrorMessage": "無効な aggType:「{aggType}」", + "xpack.triggersActionsUI.data.coreQueryParams.dateStartGTdateEndErrorMessage": "[dateStart]が[dateEnd]よりも大です", + "xpack.triggersActionsUI.data.coreQueryParams.formattedFieldErrorMessage": "{fieldName}の無効な{formatName}形式:「{fieldValue}」", + "xpack.triggersActionsUI.data.coreQueryParams.intervalRequiredErrorMessage": "[interval]: [dateStart]が[dateEnd]と等しくない場合に指定する必要があります", + "xpack.triggersActionsUI.data.coreQueryParams.invalidAggTypeErrorMessage": "無効な aggType:「{aggType}」", "xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage": "無効なthresholdComparatorが指定されました: {comparator}", - "xpack.stackAlerts.indexThreshold.invalidDateErrorMessage": "無効な日付{date}", - "xpack.stackAlerts.indexThreshold.invalidDurationErrorMessage": "無効な期間:「{duration}」", - "xpack.stackAlerts.indexThreshold.invalidGroupByErrorMessage": "無効なgroupBy:「{groupBy}」", - "xpack.stackAlerts.indexThreshold.invalidTermSizeMaximumErrorMessage": "[termSize]: {maxGroups}以下でなければなりません。", + "xpack.triggersActionsUI.data.coreQueryParams.invalidDateErrorMessage": "無効な日付{date}", + "xpack.triggersActionsUI.data.coreQueryParams.invalidDurationErrorMessage": "無効な期間:「{duration}」", + "xpack.triggersActionsUI.data.coreQueryParams.invalidGroupByErrorMessage": "無効なgroupBy:「{groupBy}」", + "xpack.triggersActionsUI.data.coreQueryParams.invalidTermSizeMaximumErrorMessage": "[termSize]: {maxGroups}以下でなければなりません。", "xpack.stackAlerts.indexThreshold.invalidThreshold2ErrorMessage": "[threshold]: 「{thresholdComparator}」比較子の場合には2つの要素が必要です", - "xpack.stackAlerts.indexThreshold.invalidTimeWindowUnitsErrorMessage": "無効なtimeWindowUnit:「{timeWindowUnit}」", - "xpack.stackAlerts.indexThreshold.maxIntervalsErrorMessage": "間隔{intervals}の計算値が{maxIntervals}よりも大です", - "xpack.stackAlerts.indexThreshold.termFieldRequiredErrorMessage": "[termField]: [groupBy]がトップのときにはtermFieldが必要です", - "xpack.stackAlerts.indexThreshold.termSizeRequiredErrorMessage": "[termSize]: [groupBy]がトップのときにはtermSizeが必要です", + "xpack.triggersActionsUI.data.coreQueryParams.invalidTimeWindowUnitsErrorMessage": "無効なtimeWindowUnit:「{timeWindowUnit}」", + "xpack.triggersActionsUI.data.coreQueryParams.maxIntervalsErrorMessage": "間隔{intervals}の計算値が{maxIntervals}よりも大です", + "xpack.triggersActionsUI.data.coreQueryParams.termFieldRequiredErrorMessage": "[termField]: [groupBy]がトップのときにはtermFieldが必要です", + "xpack.triggersActionsUI.data.coreQueryParams.termSizeRequiredErrorMessage": "[termSize]: [groupBy]がトップのときにはtermSizeが必要です", "xpack.transform.actionDeleteTransform.bulkDeleteDestinationIndexTitle": "ディスティネーションインデックスの削除", "xpack.transform.actionDeleteTransform.bulkDeleteDestIndexPatternTitle": "ディスティネーションインデックスパターンの削除", "xpack.transform.actionDeleteTransform.deleteDestinationIndexTitle": "ディスティネーションインデックス{destinationIndex}の削除", @@ -20078,42 +20078,42 @@ "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.cancelButtonLabel": "キャンセル", "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.deleteButtonLabel": "{numIdsToDelete, plural, one {{singleTitle}} other {# {multipleTitle}}}を削除 ", "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.descriptionText": "{numIdsToDelete, plural, one {a deleted {singleTitle}} other {deleted {multipleTitle}}}を回復できません。", - "xpack.triggersActionsUI.geoThreshold.boundaryNameSelect": "境界名を選択", - "xpack.triggersActionsUI.geoThreshold.boundaryNameSelectLabel": "人間が読み取れる境界名(任意)", - "xpack.triggersActionsUI.geoThreshold.delayOffset": "遅延評価オフセット", - "xpack.triggersActionsUI.geoThreshold.delayOffsetTooltip": "遅延サイクルでアラートを評価し、データレイテンシに合わせて調整します", - "xpack.triggersActionsUI.geoThreshold.entityByLabel": "グループ基準", - "xpack.triggersActionsUI.geoThreshold.entityIndexLabel": "インデックス", - "xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryGeoFieldText": "境界地理フィールドは必須です。", - "xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryIndexTitleText": "境界インデックスパターンタイトルは必須です。", - "xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryTypeText": "境界タイプは必須です。", - "xpack.triggersActionsUI.geoThreshold.error.requiredDateFieldText": "日付フィールドが必要です。", - "xpack.triggersActionsUI.geoThreshold.error.requiredEntityText": "エンティティは必須です。", - "xpack.triggersActionsUI.geoThreshold.error.requiredGeoFieldText": "地理フィールドは必須です。", - "xpack.triggersActionsUI.geoThreshold.error.requiredIndexTitleText": "インデックスパターンが必要です。", - "xpack.triggersActionsUI.geoThreshold.error.requiredTrackingEventText": "追跡イベントは必須です。", - "xpack.triggersActionsUI.geoThreshold.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。", - "xpack.triggersActionsUI.geoThreshold.geofieldLabel": "地理空間フィールド", - "xpack.triggersActionsUI.geoThreshold.indexLabel": "インデックス", - "xpack.triggersActionsUI.geoThreshold.indexPatternSelectLabel": "インデックスパターン", - "xpack.triggersActionsUI.geoThreshold.indexPatternSelectPlaceholder": "インデックスパターンを選択", - "xpack.triggersActionsUI.geoThreshold.name.trackingThreshold": "追跡しきい値", - "xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisLinkTextDescription": "インデックスパターンを作成します", - "xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisPrefixDescription": "次のことが必要です ", - "xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisSuffixDescription": " 地理空間フィールドを含む", - "xpack.triggersActionsUI.geoThreshold.noIndexPattern.getStartedLinkText": "サンプルデータセットで始めましょう。", - "xpack.triggersActionsUI.geoThreshold.noIndexPattern.hintDescription": "地理空間データセットがありませんか? ", - "xpack.triggersActionsUI.geoThreshold.noIndexPattern.messageTitle": "地理空間フィールドを含むインデックスパターンが見つかりませんでした", - "xpack.triggersActionsUI.geoThreshold.selectBoundaryIndex": "境界を選択:", - "xpack.triggersActionsUI.geoThreshold.selectEntity": "エンティティを選択", - "xpack.triggersActionsUI.geoThreshold.selectGeoLabel": "ジオフィールドを選択", - "xpack.triggersActionsUI.geoThreshold.selectIndex": "条件を定義してください", - "xpack.triggersActionsUI.geoThreshold.selectLabel": "ジオフィールドを選択", - "xpack.triggersActionsUI.geoThreshold.selectOffset": "オフセットを選択(任意)", - "xpack.triggersActionsUI.geoThreshold.selectTimeLabel": "時刻フィールドを選択", - "xpack.triggersActionsUI.geoThreshold.timeFieldLabel": "時間フィールド", - "xpack.triggersActionsUI.geoThreshold.topHitsSplitFieldSelectPlaceholder": "エンティティフィールドを選択", - "xpack.triggersActionsUI.geoThreshold.whenEntityLabel": "エンティティ", + "xpack.stackAlerts.geoThreshold.boundaryNameSelect": "境界名を選択", + "xpack.stackAlerts.geoThreshold.boundaryNameSelectLabel": "人間が読み取れる境界名(任意)", + "xpack.stackAlerts.geoThreshold.delayOffset": "遅延評価オフセット", + "xpack.stackAlerts.geoThreshold.delayOffsetTooltip": "遅延サイクルでアラートを評価し、データレイテンシに合わせて調整します", + "xpack.stackAlerts.geoThreshold.entityByLabel": "グループ基準", + "xpack.stackAlerts.geoThreshold.entityIndexLabel": "インデックス", + "xpack.stackAlerts.geoThreshold.error.requiredBoundaryGeoFieldText": "境界地理フィールドは必須です。", + "xpack.stackAlerts.geoThreshold.error.requiredBoundaryIndexTitleText": "境界インデックスパターンタイトルは必須です。", + "xpack.stackAlerts.geoThreshold.error.requiredBoundaryTypeText": "境界タイプは必須です。", + "xpack.stackAlerts.geoThreshold.error.requiredDateFieldText": "日付フィールドが必要です。", + "xpack.stackAlerts.geoThreshold.error.requiredEntityText": "エンティティは必須です。", + "xpack.stackAlerts.geoThreshold.error.requiredGeoFieldText": "地理フィールドは必須です。", + "xpack.stackAlerts.geoThreshold.error.requiredIndexTitleText": "インデックスパターンが必要です。", + "xpack.stackAlerts.geoThreshold.error.requiredTrackingEventText": "追跡イベントは必須です。", + "xpack.stackAlerts.geoThreshold.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。", + "xpack.stackAlerts.geoThreshold.geofieldLabel": "地理空間フィールド", + "xpack.stackAlerts.geoThreshold.indexLabel": "インデックス", + "xpack.stackAlerts.geoThreshold.indexPatternSelectLabel": "インデックスパターン", + "xpack.stackAlerts.geoThreshold.indexPatternSelectPlaceholder": "インデックスパターンを選択", + "xpack.stackAlerts.geoThreshold.name.trackingThreshold": "追跡しきい値", + "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisLinkTextDescription": "インデックスパターンを作成します", + "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisPrefixDescription": "次のことが必要です ", + "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisSuffixDescription": " 地理空間フィールドを含む", + "xpack.stackAlerts.geoThreshold.noIndexPattern.getStartedLinkText": "サンプルデータセットで始めましょう。", + "xpack.stackAlerts.geoThreshold.noIndexPattern.hintDescription": "地理空間データセットがありませんか? ", + "xpack.stackAlerts.geoThreshold.noIndexPattern.messageTitle": "地理空間フィールドを含むインデックスパターンが見つかりませんでした", + "xpack.stackAlerts.geoThreshold.selectBoundaryIndex": "境界を選択:", + "xpack.stackAlerts.geoThreshold.selectEntity": "エンティティを選択", + "xpack.stackAlerts.geoThreshold.selectGeoLabel": "ジオフィールドを選択", + "xpack.stackAlerts.geoThreshold.selectIndex": "条件を定義してください", + "xpack.stackAlerts.geoThreshold.selectLabel": "ジオフィールドを選択", + "xpack.stackAlerts.geoThreshold.selectOffset": "オフセットを選択(任意)", + "xpack.stackAlerts.geoThreshold.selectTimeLabel": "時刻フィールドを選択", + "xpack.stackAlerts.geoThreshold.timeFieldLabel": "時間フィールド", + "xpack.stackAlerts.geoThreshold.topHitsSplitFieldSelectPlaceholder": "エンティティフィールドを選択", + "xpack.stackAlerts.geoThreshold.whenEntityLabel": "エンティティ", "xpack.triggersActionsUI.home.alertsTabTitle": "アラート", "xpack.triggersActionsUI.home.appTitle": "アラートとアクション", "xpack.triggersActionsUI.home.breadcrumbTitle": "アラートとアクション", @@ -20156,15 +20156,15 @@ "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHeaderValueText": "値が必要です。", "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText": "メソッドが必要です", "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText": "パスワードが必要です。", - "xpack.triggersActionsUI.sections.addAlert.error.greaterThenThreshold0Text": "しきい値 1 はしきい値 0 よりも大きい値にしてください。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredAggFieldText": "集約フィールドが必要です。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredIndexText": "インデックスが必要です。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredTermSizedText": "用語サイズが必要です。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredThreshold0Text": "しきい値 0 が必要です。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredThreshold1Text": "しきい値 1 が必要です。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredTimeFieldText": "時間フィールドが必要です。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredTimeWindowSizeText": "時間ウィンドウサイズが必要です。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredtTermFieldText": "用語フィールドが必要です。", + "xpack.stackAlerts.threshold.ui.validation.error.greaterThenThreshold0Text": "しきい値 1 はしきい値 0 よりも大きい値にしてください。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredAggFieldText": "集約フィールドが必要です。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredIndexText": "インデックスが必要です。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredTermSizedText": "用語サイズが必要です。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold0Text": "しきい値 0 が必要です。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold1Text": "しきい値 1 が必要です。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredTimeFieldText": "時間フィールドが必要です。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredTimeWindowSizeText": "時間ウィンドウサイズが必要です。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredTermFieldText": "用語フィールドが必要です。", "xpack.triggersActionsUI.sections.addConnectorForm.flyoutTitle": "{actionTypeName} コネクタ", "xpack.triggersActionsUI.sections.addConnectorForm.selectConnectorFlyoutTitle": "コネクターを選択", "xpack.triggersActionsUI.sections.addConnectorForm.updateErrorNotificationText": "コネクターを作成できません。", @@ -20173,27 +20173,27 @@ "xpack.triggersActionsUI.sections.addModalConnectorForm.flyoutTitle": "{actionTypeName} コネクター", "xpack.triggersActionsUI.sections.addModalConnectorForm.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.addModalConnectorForm.updateSuccessNotificationText": "「{connectorName}」を作成しました", - "xpack.triggersActionsUI.sections.alertAdd.conditionPrompt": "条件を定義してください", - "xpack.triggersActionsUI.sections.alertAdd.errorLoadingAlertVisualizationTitle": "アラートビジュアライゼーションを読み込めません", + "xpack.stackAlerts.threshold.ui.conditionPrompt": "条件を定義してください", + "xpack.stackAlerts.threshold.ui.visualization.errorLoadingAlertVisualizationTitle": "アラートビジュアライゼーションを読み込めません", "xpack.triggersActionsUI.sections.alertAdd.flyoutTitle": "アラートの作成", - "xpack.triggersActionsUI.sections.alertAdd.geoThreshold.closePopoverLabel": "閉じる", - "xpack.triggersActionsUI.sections.alertAdd.loadingAlertVisualizationDescription": "アラートビジュアライゼーションを読み込み中...", + "xpack.stackAlerts.geoThreshold.ui.expressionPopover.closePopoverLabel": "閉じる", + "xpack.stackAlerts.threshold.ui.visualization.loadingAlertVisualizationDescription": "アラートビジュアライゼーションを読み込み中...", "xpack.triggersActionsUI.sections.alertAdd.operationName": "作成", - "xpack.triggersActionsUI.sections.alertAdd.previewAlertVisualizationDescription": "プレビューを生成するための式を完成します。", + "xpack.stackAlerts.threshold.ui.previewAlertVisualizationDescription": "プレビューを生成するための式を完成します。", "xpack.triggersActionsUI.sections.alertAdd.saveErrorNotificationText": "アラートを作成できません。", "xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText": "「{alertName}」 を保存しました", - "xpack.triggersActionsUI.sections.alertAdd.selectIndex": "インデックスを選択してください", - "xpack.triggersActionsUI.sections.alertAdd.threshold.closeIndexPopoverLabel": "閉じる", - "xpack.triggersActionsUI.sections.alertAdd.threshold.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。", - "xpack.triggersActionsUI.sections.alertAdd.threshold.howToBroadenSearchQueryDescription": "* で検索クエリの範囲を広げます。", - "xpack.triggersActionsUI.sections.alertAdd.threshold.indexButtonLabel": "インデックス", - "xpack.triggersActionsUI.sections.alertAdd.threshold.indexLabel": "インデックス", - "xpack.triggersActionsUI.sections.alertAdd.threshold.indicesToQueryLabel": "クエリを実行するインデックス", - "xpack.triggersActionsUI.sections.alertAdd.threshold.timeFieldLabel": "時間フィールド", - "xpack.triggersActionsUI.sections.alertAdd.threshold.timeFieldOptionLabel": "フィールドを選択", - "xpack.triggersActionsUI.sections.alertAdd.thresholdPreviewChart.dataDoesNotExistTextMessage": "時間範囲とフィルターが正しいことを確認してください。", - "xpack.triggersActionsUI.sections.alertAdd.thresholdPreviewChart.noDataTitle": "このクエリに一致するデータはありません", - "xpack.triggersActionsUI.sections.alertAdd.unableToLoadVisualizationMessage": "ビジュアライゼーションを読み込めません", + "xpack.stackAlerts.threshold.ui.selectIndex": "インデックスを選択してください", + "xpack.stackAlerts.threshold.ui.alertParams.closeIndexPopoverLabel": "閉じる", + "xpack.stackAlerts.threshold.ui.alertParams.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。", + "xpack.stackAlerts.threshold.ui.alertParams.howToBroadenSearchQueryDescription": "* で検索クエリの範囲を広げます。", + "xpack.stackAlerts.threshold.ui.alertParams.indexButtonLabel": "インデックス", + "xpack.stackAlerts.threshold.ui.alertParams.indexLabel": "インデックス", + "xpack.stackAlerts.threshold.ui.alertParams.indicesToQueryLabel": "クエリを実行するインデックス", + "xpack.stackAlerts.threshold.ui.alertParams.timeFieldLabel": "時間フィールド", + "xpack.triggersActionsUI.sections.alertAdd.indexControls.timeFieldOptionLabel": "フィールドを選択", + "xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.dataDoesNotExistTextMessage": "時間範囲とフィルターが正しいことを確認してください。", + "xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.noDataTitle": "このクエリに一致するデータはありません", + "xpack.stackAlerts.threshold.ui.visualization.unableToLoadVisualizationMessage": "ビジュアライゼーションを読み込めません", "xpack.triggersActionsUI.sections.alertDetails.alertInstances.disabledAlert": "このアラートは無効になっていて再表示できません。[↑ を無効にする]を切り替えてアクティブにします。", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.duration": "期間", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.instance": "インスタンス", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8ae964d9ee7d09..48654a5ec5ff43 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -19544,24 +19544,24 @@ "xpack.stackAlerts.indexThreshold.actionVariableContextThresholdLabel": "用作阈值的值数组;“between”和“notBetween”需要两个值,其他则需要一个值。", "xpack.stackAlerts.indexThreshold.actionVariableContextTitleLabel": "告警的预构造标题。", "xpack.stackAlerts.indexThreshold.actionVariableContextValueLabel": "超过阈值的值。", - "xpack.stackAlerts.indexThreshold.aggTypeRequiredErrorMessage": "[aggField]:当 [aggType] 为“{aggType}”时必须有值", + "xpack.triggersActionsUI.data.coreQueryParams.aggTypeRequiredErrorMessage": "[aggField]:当 [aggType] 为“{aggType}”时必须有值", "xpack.stackAlerts.indexThreshold.alertTypeContextMessageDescription": "告警 {name} 组 {group} 值 {value} 在 {window} 于 {date}超过了阈值 {function}", "xpack.stackAlerts.indexThreshold.alertTypeContextSubjectTitle": "告警 {name} 组 {group} 超过了阈值", "xpack.stackAlerts.indexThreshold.alertTypeTitle": "索引阈值", - "xpack.stackAlerts.indexThreshold.dateStartGTdateEndErrorMessage": "[dateStart]:晚于 [dateEnd]", - "xpack.stackAlerts.indexThreshold.formattedFieldErrorMessage": "{fieldName} 的 {formatName} 格式无效:“{fieldValue}”", - "xpack.stackAlerts.indexThreshold.intervalRequiredErrorMessage": "[interval]:如果 [dateStart] 不等于 [dateEnd],则必须指定", - "xpack.stackAlerts.indexThreshold.invalidAggTypeErrorMessage": "aggType 无效:“{aggType}”", + "xpack.triggersActionsUI.data.coreQueryParams.dateStartGTdateEndErrorMessage": "[dateStart]:晚于 [dateEnd]", + "xpack.triggersActionsUI.data.coreQueryParams.formattedFieldErrorMessage": "{fieldName} 的 {formatName} 格式无效:“{fieldValue}”", + "xpack.triggersActionsUI.data.coreQueryParams.intervalRequiredErrorMessage": "[interval]:如果 [dateStart] 不等于 [dateEnd],则必须指定", + "xpack.triggersActionsUI.data.coreQueryParams.invalidAggTypeErrorMessage": "aggType 无效:“{aggType}”", "xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage": "指定的 thresholdComparator 无效:{comparator}", - "xpack.stackAlerts.indexThreshold.invalidDateErrorMessage": "日期 {date} 无效", - "xpack.stackAlerts.indexThreshold.invalidDurationErrorMessage": "持续时间无效:“{duration}”", - "xpack.stackAlerts.indexThreshold.invalidGroupByErrorMessage": "groupBy 无效:“{groupBy}”", - "xpack.stackAlerts.indexThreshold.invalidTermSizeMaximumErrorMessage": "[termSize]:必须小于或等于 {maxGroups}", + "xpack.triggersActionsUI.data.coreQueryParams.invalidDateErrorMessage": "日期 {date} 无效", + "xpack.triggersActionsUI.data.coreQueryParams.invalidDurationErrorMessage": "持续时间无效:“{duration}”", + "xpack.triggersActionsUI.data.coreQueryParams.invalidGroupByErrorMessage": "groupBy 无效:“{groupBy}”", + "xpack.triggersActionsUI.data.coreQueryParams.invalidTermSizeMaximumErrorMessage": "[termSize]:必须小于或等于 {maxGroups}", "xpack.stackAlerts.indexThreshold.invalidThreshold2ErrorMessage": "[threshold]:对于“{thresholdComparator}”比较运算符,必须包含两个元素", - "xpack.stackAlerts.indexThreshold.invalidTimeWindowUnitsErrorMessage": "timeWindowUnit 无效:“{timeWindowUnit}”", - "xpack.stackAlerts.indexThreshold.maxIntervalsErrorMessage": "时间间隔 {intervals} 的计算数目大于最大值 {maxIntervals}", - "xpack.stackAlerts.indexThreshold.termFieldRequiredErrorMessage": "[termField]:[groupBy] 为 top 时,termField 为必需", - "xpack.stackAlerts.indexThreshold.termSizeRequiredErrorMessage": "[termSize]:[groupBy] 为 top 时,termSize 为必需", + "xpack.triggersActionsUI.data.coreQueryParams.invalidTimeWindowUnitsErrorMessage": "timeWindowUnit 无效:“{timeWindowUnit}”", + "xpack.triggersActionsUI.data.coreQueryParams.maxIntervalsErrorMessage": "时间间隔 {intervals} 的计算数目大于最大值 {maxIntervals}", + "xpack.triggersActionsUI.data.coreQueryParams.termFieldRequiredErrorMessage": "[termField]:[groupBy] 为 top 时,termField 为必需", + "xpack.triggersActionsUI.data.coreQueryParams.termSizeRequiredErrorMessage": "[termSize]:[groupBy] 为 top 时,termSize 为必需", "xpack.transform.actionDeleteTransform.bulkDeleteDestinationIndexTitle": "删除目标索引", "xpack.transform.actionDeleteTransform.bulkDeleteDestIndexPatternTitle": "删除目标索引模式", "xpack.transform.actionDeleteTransform.deleteDestinationIndexTitle": "删除目标索引 {destinationIndex}", @@ -20097,42 +20097,42 @@ "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.cancelButtonLabel": "取消", "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.deleteButtonLabel": "删除{numIdsToDelete, plural, one {{singleTitle}} other { # 个{multipleTitle}}} ", "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.descriptionText": "无法恢复{numIdsToDelete, plural, one {删除的{singleTitle}} other {删除的{multipleTitle}}}。", - "xpack.triggersActionsUI.geoThreshold.boundaryNameSelect": "选择边界名称", - "xpack.triggersActionsUI.geoThreshold.boundaryNameSelectLabel": "可人工读取的边界名称(可选)", - "xpack.triggersActionsUI.geoThreshold.delayOffset": "已延迟的评估偏移", - "xpack.triggersActionsUI.geoThreshold.delayOffsetTooltip": "评估延迟周期内的告警,以针对数据延迟进行调整", - "xpack.triggersActionsUI.geoThreshold.entityByLabel": "方式", - "xpack.triggersActionsUI.geoThreshold.entityIndexLabel": "索引", - "xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryGeoFieldText": "“边界地理”字段必填。", - "xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryIndexTitleText": "“边界索引模式标题”必填。", - "xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryTypeText": "“边界类型”必填。", - "xpack.triggersActionsUI.geoThreshold.error.requiredDateFieldText": "“日期”字段必填。", - "xpack.triggersActionsUI.geoThreshold.error.requiredEntityText": "“实体”必填。", - "xpack.triggersActionsUI.geoThreshold.error.requiredGeoFieldText": "“地理”字段必填。", - "xpack.triggersActionsUI.geoThreshold.error.requiredIndexTitleText": "“索引模式”必填。", - "xpack.triggersActionsUI.geoThreshold.error.requiredTrackingEventText": "“跟踪事件”必填。", - "xpack.triggersActionsUI.geoThreshold.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。", - "xpack.triggersActionsUI.geoThreshold.geofieldLabel": "地理空间字段", - "xpack.triggersActionsUI.geoThreshold.indexLabel": "索引", - "xpack.triggersActionsUI.geoThreshold.indexPatternSelectLabel": "索引模式", - "xpack.triggersActionsUI.geoThreshold.indexPatternSelectPlaceholder": "选择索引模式", - "xpack.triggersActionsUI.geoThreshold.name.trackingThreshold": "跟踪阈值", - "xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisLinkTextDescription": "创建索引模式", - "xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisPrefixDescription": "您将需要 ", - "xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisSuffixDescription": " (包含地理空间字段)。", - "xpack.triggersActionsUI.geoThreshold.noIndexPattern.getStartedLinkText": "开始使用一些样例数据集。", - "xpack.triggersActionsUI.geoThreshold.noIndexPattern.hintDescription": "没有任何地理空间数据集? ", - "xpack.triggersActionsUI.geoThreshold.noIndexPattern.messageTitle": "找不到任何具有地理空间字段的索引模式", - "xpack.triggersActionsUI.geoThreshold.selectBoundaryIndex": "选择边界:", - "xpack.triggersActionsUI.geoThreshold.selectEntity": "选择实体", - "xpack.triggersActionsUI.geoThreshold.selectGeoLabel": "选择地理字段", - "xpack.triggersActionsUI.geoThreshold.selectIndex": "定义条件", - "xpack.triggersActionsUI.geoThreshold.selectLabel": "选择地理字段", - "xpack.triggersActionsUI.geoThreshold.selectOffset": "选择偏移(可选)", - "xpack.triggersActionsUI.geoThreshold.selectTimeLabel": "选择时间字段", - "xpack.triggersActionsUI.geoThreshold.timeFieldLabel": "时间字段", - "xpack.triggersActionsUI.geoThreshold.topHitsSplitFieldSelectPlaceholder": "选择实体字段", - "xpack.triggersActionsUI.geoThreshold.whenEntityLabel": "当实体", + "xpack.stackAlerts.geoThreshold.boundaryNameSelect": "选择边界名称", + "xpack.stackAlerts.geoThreshold.boundaryNameSelectLabel": "可人工读取的边界名称(可选)", + "xpack.stackAlerts.geoThreshold.delayOffset": "已延迟的评估偏移", + "xpack.stackAlerts.geoThreshold.delayOffsetTooltip": "评估延迟周期内的告警,以针对数据延迟进行调整", + "xpack.stackAlerts.geoThreshold.entityByLabel": "方式", + "xpack.stackAlerts.geoThreshold.entityIndexLabel": "索引", + "xpack.stackAlerts.geoThreshold.error.requiredBoundaryGeoFieldText": "“边界地理”字段必填。", + "xpack.stackAlerts.geoThreshold.error.requiredBoundaryIndexTitleText": "“边界索引模式标题”必填。", + "xpack.stackAlerts.geoThreshold.error.requiredBoundaryTypeText": "“边界类型”必填。", + "xpack.stackAlerts.geoThreshold.error.requiredDateFieldText": "“日期”字段必填。", + "xpack.stackAlerts.geoThreshold.error.requiredEntityText": "“实体”必填。", + "xpack.stackAlerts.geoThreshold.error.requiredGeoFieldText": "“地理”字段必填。", + "xpack.stackAlerts.geoThreshold.error.requiredIndexTitleText": "“索引模式”必填。", + "xpack.stackAlerts.geoThreshold.error.requiredTrackingEventText": "“跟踪事件”必填。", + "xpack.stackAlerts.geoThreshold.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。", + "xpack.stackAlerts.geoThreshold.geofieldLabel": "地理空间字段", + "xpack.stackAlerts.geoThreshold.indexLabel": "索引", + "xpack.stackAlerts.geoThreshold.indexPatternSelectLabel": "索引模式", + "xpack.stackAlerts.geoThreshold.indexPatternSelectPlaceholder": "选择索引模式", + "xpack.stackAlerts.geoThreshold.name.trackingThreshold": "跟踪阈值", + "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisLinkTextDescription": "创建索引模式", + "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisPrefixDescription": "您将需要 ", + "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisSuffixDescription": " (包含地理空间字段)。", + "xpack.stackAlerts.geoThreshold.noIndexPattern.getStartedLinkText": "开始使用一些样例数据集。", + "xpack.stackAlerts.geoThreshold.noIndexPattern.hintDescription": "没有任何地理空间数据集? ", + "xpack.stackAlerts.geoThreshold.noIndexPattern.messageTitle": "找不到任何具有地理空间字段的索引模式", + "xpack.stackAlerts.geoThreshold.selectBoundaryIndex": "选择边界:", + "xpack.stackAlerts.geoThreshold.selectEntity": "选择实体", + "xpack.stackAlerts.geoThreshold.selectGeoLabel": "选择地理字段", + "xpack.stackAlerts.geoThreshold.selectIndex": "定义条件", + "xpack.stackAlerts.geoThreshold.selectLabel": "选择地理字段", + "xpack.stackAlerts.geoThreshold.selectOffset": "选择偏移(可选)", + "xpack.stackAlerts.geoThreshold.selectTimeLabel": "选择时间字段", + "xpack.stackAlerts.geoThreshold.timeFieldLabel": "时间字段", + "xpack.stackAlerts.geoThreshold.topHitsSplitFieldSelectPlaceholder": "选择实体字段", + "xpack.stackAlerts.geoThreshold.whenEntityLabel": "当实体", "xpack.triggersActionsUI.home.alertsTabTitle": "告警", "xpack.triggersActionsUI.home.appTitle": "告警和操作", "xpack.triggersActionsUI.home.breadcrumbTitle": "告警和操作", @@ -20176,15 +20176,15 @@ "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHeaderValueText": "“值”必填。", "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText": "“方法”必填", "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText": "“密码”必填。", - "xpack.triggersActionsUI.sections.addAlert.error.greaterThenThreshold0Text": "阈值 1 应 > 阈值 0。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredAggFieldText": "聚合字段必填。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredIndexText": "“索引”必填。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredTermSizedText": "“词大小”必填。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredThreshold0Text": "阈值 0 必填。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredThreshold1Text": "阈值 1 必填。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredTimeFieldText": "时间字段必填。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredTimeWindowSizeText": "“时间窗大小”必填。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredtTermFieldText": "词字段必填。", + "xpack.stackAlerts.threshold.ui.validation.error.greaterThenThreshold0Text": "阈值 1 应 > 阈值 0。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredAggFieldText": "聚合字段必填。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredIndexText": "“索引”必填。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredTermSizedText": "“词大小”必填。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold0Text": "阈值 0 必填。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold1Text": "阈值 1 必填。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredTimeFieldText": "时间字段必填。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredTimeWindowSizeText": "“时间窗大小”必填。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredTermFieldText": "词字段必填。", "xpack.triggersActionsUI.sections.addConnectorForm.flyoutTitle": "{actionTypeName} 连接器", "xpack.triggersActionsUI.sections.addConnectorForm.selectConnectorFlyoutTitle": "选择连接器", "xpack.triggersActionsUI.sections.addConnectorForm.updateErrorNotificationText": "无法创建连接器。", @@ -20193,27 +20193,27 @@ "xpack.triggersActionsUI.sections.addModalConnectorForm.flyoutTitle": "{actionTypeName} 连接器", "xpack.triggersActionsUI.sections.addModalConnectorForm.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.addModalConnectorForm.updateSuccessNotificationText": "已创建“{connectorName}”", - "xpack.triggersActionsUI.sections.alertAdd.conditionPrompt": "定义条件", - "xpack.triggersActionsUI.sections.alertAdd.errorLoadingAlertVisualizationTitle": "无法加载告警可视化", + "xpack.stackAlerts.threshold.ui.conditionPrompt": "定义条件", + "xpack.stackAlerts.threshold.ui.visualization.errorLoadingAlertVisualizationTitle": "无法加载告警可视化", "xpack.triggersActionsUI.sections.alertAdd.flyoutTitle": "创建告警", - "xpack.triggersActionsUI.sections.alertAdd.geoThreshold.closePopoverLabel": "关闭", - "xpack.triggersActionsUI.sections.alertAdd.loadingAlertVisualizationDescription": "正在加载告警可视化……", + "xpack.stackAlerts.geoThreshold.ui.expressionPopover.closePopoverLabel": "关闭", + "xpack.stackAlerts.threshold.ui.visualization.loadingAlertVisualizationDescription": "正在加载告警可视化……", "xpack.triggersActionsUI.sections.alertAdd.operationName": "创建", - "xpack.triggersActionsUI.sections.alertAdd.previewAlertVisualizationDescription": "完成表达式以生成预览。", + "xpack.stackAlerts.threshold.ui.previewAlertVisualizationDescription": "完成表达式以生成预览。", "xpack.triggersActionsUI.sections.alertAdd.saveErrorNotificationText": "无法创建告警。", "xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText": "已保存“{alertName}”", - "xpack.triggersActionsUI.sections.alertAdd.selectIndex": "选择索引", - "xpack.triggersActionsUI.sections.alertAdd.threshold.closeIndexPopoverLabel": "关闭", - "xpack.triggersActionsUI.sections.alertAdd.threshold.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。", - "xpack.triggersActionsUI.sections.alertAdd.threshold.howToBroadenSearchQueryDescription": "使用 * 可扩大您的查询范围。", - "xpack.triggersActionsUI.sections.alertAdd.threshold.indexButtonLabel": "索引", - "xpack.triggersActionsUI.sections.alertAdd.threshold.indexLabel": "索引", - "xpack.triggersActionsUI.sections.alertAdd.threshold.indicesToQueryLabel": "要查询的索引", - "xpack.triggersActionsUI.sections.alertAdd.threshold.timeFieldLabel": "时间字段", - "xpack.triggersActionsUI.sections.alertAdd.threshold.timeFieldOptionLabel": "选择字段", - "xpack.triggersActionsUI.sections.alertAdd.thresholdPreviewChart.dataDoesNotExistTextMessage": "确认您的时间范围和筛选正确。", - "xpack.triggersActionsUI.sections.alertAdd.thresholdPreviewChart.noDataTitle": "没有数据匹配此查询", - "xpack.triggersActionsUI.sections.alertAdd.unableToLoadVisualizationMessage": "无法加载可视化", + "xpack.stackAlerts.threshold.ui.selectIndex": "选择索引", + "xpack.stackAlerts.threshold.ui.alertParams.closeIndexPopoverLabel": "关闭", + "xpack.stackAlerts.threshold.ui.alertParams.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。", + "xpack.stackAlerts.threshold.ui.alertParams.howToBroadenSearchQueryDescription": "使用 * 可扩大您的查询范围。", + "xpack.stackAlerts.threshold.ui.alertParams.indexButtonLabel": "索引", + "xpack.stackAlerts.threshold.ui.alertParams.indexLabel": "索引", + "xpack.stackAlerts.threshold.ui.alertParams.indicesToQueryLabel": "要查询的索引", + "xpack.stackAlerts.threshold.ui.alertParams.timeFieldLabel": "时间字段", + "xpack.triggersActionsUI.sections.alertAdd.indexControls.timeFieldOptionLabel": "选择字段", + "xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.dataDoesNotExistTextMessage": "确认您的时间范围和筛选正确。", + "xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.noDataTitle": "没有数据匹配此查询", + "xpack.stackAlerts.threshold.ui.visualization.unableToLoadVisualizationMessage": "无法加载可视化", "xpack.triggersActionsUI.sections.alertDetails.alertInstances.disabledAlert": "此告警已禁用,无法显示。切换禁用 ↑ 以激活。", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.duration": "持续时间", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.instance": "实例", diff --git a/x-pack/plugins/triggers_actions_ui/README.md b/x-pack/plugins/triggers_actions_ui/README.md index 32e157255c0cc2..ef81065608ad41 100644 --- a/x-pack/plugins/triggers_actions_ui/README.md +++ b/x-pack/plugins/triggers_actions_ui/README.md @@ -220,7 +220,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent .... diff --git a/x-pack/plugins/stack_alerts/common/alert_types/index_threshold/index.ts b/x-pack/plugins/triggers_actions_ui/common/data/index.ts similarity index 100% rename from x-pack/plugins/stack_alerts/common/alert_types/index_threshold/index.ts rename to x-pack/plugins/triggers_actions_ui/common/data/index.ts diff --git a/x-pack/plugins/triggers_actions_ui/common/index.ts b/x-pack/plugins/triggers_actions_ui/common/index.ts new file mode 100644 index 00000000000000..5775cc2454a7e7 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/common/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 * from './data'; diff --git a/x-pack/plugins/triggers_actions_ui/kibana.json b/x-pack/plugins/triggers_actions_ui/kibana.json index 72e1f0be5f7f4c..9d79ab9232bf38 100644 --- a/x-pack/plugins/triggers_actions_ui/kibana.json +++ b/x-pack/plugins/triggers_actions_ui/kibana.json @@ -3,8 +3,8 @@ "version": "kibana", "server": true, "ui": true, - "optionalPlugins": ["alerts", "stackAlerts", "features", "home"], - "requiredPlugins": ["management", "charts", "data", "kibanaReact"], + "optionalPlugins": ["alerts", "features", "home"], + "requiredPlugins": ["management", "charts", "data"], "configPath": ["xpack", "trigger_actions_ui"], "extraPublicDirs": ["public/common", "public/common/constants"], "requiredBundles": ["home", "alerts", "esUiShared"] diff --git a/x-pack/plugins/triggers_actions_ui/public/application/boot.tsx b/x-pack/plugins/triggers_actions_ui/public/application/boot.tsx index 80f9ac532d1c95..bb46fd02a98a94 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/boot.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/boot.tsx @@ -9,7 +9,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { SavedObjectsClientContract } from 'src/core/public'; import { App, AppDeps } from './app'; -import { setSavedObjectsClient } from '../common/lib/index_threshold_api'; +import { setSavedObjectsClient } from '../common/lib/data_apis'; interface BootDeps extends AppDeps { element: HTMLElement; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/index.ts index 32b323334654ef..86c33a373753fc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/index.ts @@ -5,6 +5,10 @@ */ export * from './expression_items'; +export * from './constants'; +export * from './index_controls'; +export * from './lib'; +export * from './types'; export { connectorConfiguration as ServiceNowConnectorConfiguration } from '../application/components/builtin_action_types/servicenow/config'; export { connectorConfiguration as JiraConnectorConfiguration } from '../application/components/builtin_action_types/jira/config'; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/index_controls/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/index_controls/index.ts index da332aa326ccf5..8d10e531930ccf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/index_controls/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/index_controls/index.ts @@ -9,10 +9,10 @@ import { HttpSetup } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { loadIndexPatterns, - getMatchingIndicesForThresholdAlertType, - getThresholdAlertTypeFields, + getMatchingIndices, + getESIndexFields, getSavedObjectsClient, -} from '../lib/index_threshold_api'; +} from '../lib/data_apis'; export interface IOption { label: string; @@ -39,7 +39,7 @@ export const getIndexOptions = async ( return options; } - const matchingIndices = (await getMatchingIndicesForThresholdAlertType({ + const matchingIndices = (await getMatchingIndices({ pattern, http, })) as string[]; @@ -85,12 +85,15 @@ export const getIndexOptions = async ( }; export const getFields = async (http: HttpSetup, indexes: string[]) => { - return await getThresholdAlertTypeFields({ indexes, http }); + return await getESIndexFields({ indexes, http }); }; export const firstFieldOption = { - text: i18n.translate('xpack.triggersActionsUI.sections.alertAdd.threshold.timeFieldOptionLabel', { - defaultMessage: 'Select a field', - }), + text: i18n.translate( + 'xpack.triggersActionsUI.sections.alertAdd.indexControls.timeFieldOptionLabel', + { + defaultMessage: 'Select a field', + } + ), value: '', }; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.ts new file mode 100644 index 00000000000000..573d306ae55503 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.ts @@ -0,0 +1,67 @@ +/* + * 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 { HttpSetup } from 'kibana/public'; + +const DATA_API_ROOT = '/api/triggers_actions_ui/data'; + +export async function getMatchingIndices({ + pattern, + http, +}: { + pattern: string; + http: HttpSetup; +}): Promise> { + if (!pattern.startsWith('*')) { + pattern = `*${pattern}`; + } + if (!pattern.endsWith('*')) { + pattern = `${pattern}*`; + } + const { indices } = await http.post(`${DATA_API_ROOT}/_indices`, { + body: JSON.stringify({ pattern }), + }); + return indices; +} + +export async function getESIndexFields({ + indexes, + http, +}: { + indexes: string[]; + http: HttpSetup; +}): Promise< + Array<{ + name: string; + type: string; + normalizedType: string; + searchable: boolean; + aggregatable: boolean; + }> +> { + const { fields } = await http.post(`${DATA_API_ROOT}/_fields`, { + body: JSON.stringify({ indexPatterns: indexes }), + }); + return fields; +} + +let savedObjectsClient: any; + +export const setSavedObjectsClient = (aSavedObjectsClient: any) => { + savedObjectsClient = aSavedObjectsClient; +}; + +export const getSavedObjectsClient = () => { + return savedObjectsClient; +}; + +export const loadIndexPatterns = async () => { + const { savedObjects } = await getSavedObjectsClient().find({ + type: 'index-pattern', + fields: ['title'], + perPage: 10000, + }); + return savedObjects; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/index.ts new file mode 100644 index 00000000000000..7671e239f8fff4 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/index.ts @@ -0,0 +1,6 @@ +/* + * 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 { getTimeFieldOptions, getTimeOptions } from './get_time_options'; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/index_threshold_api.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/index_threshold_api.ts deleted file mode 100644 index 11d273fcb7a420..00000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/common/lib/index_threshold_api.ts +++ /dev/null @@ -1,91 +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 { HttpSetup } from 'kibana/public'; -import { TimeSeriesResult } from '../../../../stack_alerts/common/alert_types/index_threshold'; - -const INDEX_THRESHOLD_API_ROOT = '/api/stack_alerts/index_threshold'; - -export async function getMatchingIndicesForThresholdAlertType({ - pattern, - http, -}: { - pattern: string; - http: HttpSetup; -}): Promise> { - if (!pattern.startsWith('*')) { - pattern = `*${pattern}`; - } - if (!pattern.endsWith('*')) { - pattern = `${pattern}*`; - } - const { indices } = await http.post(`${INDEX_THRESHOLD_API_ROOT}/_indices`, { - body: JSON.stringify({ pattern }), - }); - return indices; -} - -export async function getThresholdAlertTypeFields({ - indexes, - http, -}: { - indexes: string[]; - http: HttpSetup; -}): Promise> { - const { fields } = await http.post(`${INDEX_THRESHOLD_API_ROOT}/_fields`, { - body: JSON.stringify({ indexPatterns: indexes }), - }); - return fields; -} - -let savedObjectsClient: any; - -export const setSavedObjectsClient = (aSavedObjectsClient: any) => { - savedObjectsClient = aSavedObjectsClient; -}; - -export const getSavedObjectsClient = () => { - return savedObjectsClient; -}; - -export const loadIndexPatterns = async () => { - const { savedObjects } = await getSavedObjectsClient().find({ - type: 'index-pattern', - fields: ['title'], - perPage: 10000, - }); - return savedObjects; -}; - -interface GetThresholdAlertVisualizationDataParams { - model: any; - visualizeOptions: any; - http: HttpSetup; -} - -export async function getThresholdAlertVisualizationData({ - model, - visualizeOptions, - http, -}: GetThresholdAlertVisualizationDataParams): Promise { - const timeSeriesQueryParams = { - index: model.index, - timeField: model.timeField, - aggType: model.aggType, - aggField: model.aggField, - groupBy: model.groupBy, - termField: model.termField, - termSize: model.termSize, - timeWindowSize: model.timeWindowSize, - timeWindowUnit: model.timeWindowUnit, - dateStart: new Date(visualizeOptions.rangeFrom).toISOString(), - dateEnd: new Date(visualizeOptions.rangeTo).toISOString(), - interval: visualizeOptions.interval, - }; - - return await http.post(`${INDEX_THRESHOLD_API_ROOT}/_time_series_query`, { - body: JSON.stringify(timeSeriesQueryParams), - }); -} diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index a28b10683c28f9..3794112e1d5023 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'src/core/public'; import { Plugin } from './plugin'; export { AlertsContextProvider, AlertsContextValue } from './application/context/alerts_context'; @@ -22,15 +21,17 @@ export { ValidationResult, ActionVariable, ActionConnector, + IErrorObject, } from './types'; export { ConnectorAddFlyout, ConnectorEditFlyout, } from './application/sections/action_connector_form'; export { loadActionTypes } from './application/lib/action_connector_api'; +export * from './common'; -export function plugin(ctx: PluginInitializerContext) { - return new Plugin(ctx); +export function plugin() { + return new Plugin(); } export { Plugin }; diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 61cd7699c50c5b..2d93d368ad8e54 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -4,17 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - CoreSetup, - CoreStart, - Plugin as CorePlugin, - PluginInitializerContext, -} from 'src/core/public'; +import { CoreSetup, CoreStart, Plugin as CorePlugin } from 'src/core/public'; import { i18n } from '@kbn/i18n'; import { FeaturesPluginStart } from '../../features/public'; import { registerBuiltInActionTypes } from './application/components/builtin_action_types'; -import { registerBuiltInAlertTypes } from './application/components/builtin_alert_types'; import { ActionTypeModel, AlertTypeModel } from './types'; import { TypeRegistry } from './application/type_registry'; import { @@ -29,10 +23,6 @@ import { ChartsPluginStart } from '../../../../src/plugins/charts/public'; import { PluginStartContract as AlertingStart } from '../../alerts/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; -export interface TriggersActionsUiConfigType { - enableGeoTrackingThresholdAlert: boolean; -} - export interface TriggersAndActionsUIPublicPluginSetup { actionTypeRegistry: TypeRegistry; alertTypeRegistry: TypeRegistry; @@ -66,14 +56,10 @@ export class Plugin > { private actionTypeRegistry: TypeRegistry; private alertTypeRegistry: TypeRegistry; - private initializerContext: PluginInitializerContext; - constructor(initializerContext: PluginInitializerContext) { + constructor() { this.actionTypeRegistry = new TypeRegistry(); - this.alertTypeRegistry = new TypeRegistry(); - - this.initializerContext = initializerContext; } public setup(core: CoreSetup, plugins: PluginsSetup): TriggersAndActionsUIPublicPluginSetup { @@ -142,11 +128,6 @@ export class Plugin actionTypeRegistry: this.actionTypeRegistry, }); - registerBuiltInAlertTypes({ - alertTypeRegistry: this.alertTypeRegistry, - triggerActionsUiConfig: this.initializerContext.config.get(), - }); - return { actionTypeRegistry: this.actionTypeRegistry, alertTypeRegistry: this.alertTypeRegistry, diff --git a/x-pack/plugins/triggers_actions_ui/server/data/README.md b/x-pack/plugins/triggers_actions_ui/server/data/README.md new file mode 100644 index 00000000000000..78577f0783008e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/server/data/README.md @@ -0,0 +1,228 @@ +# Data Apis + +The TriggersActionsUi plugin's Data Apis back the functionality needed by the Index Threshold Stack Alert. + +## http endpoints + +The following endpoints are provided for this alert type: + +- `POST /api/triggers_actions_ui/data/_indices` +- `POST /api/triggers_actions_ui/data/_fields` +- `POST /api/triggers_actions_ui/data/_time_series_query` + +### `POST .../_indices` + +This HTTP endpoint is provided for the alerting ui to list the available +"index names" for the user to select to use with the alert. This API also +returns aliases which match the supplied pattern. + +The request body is expected to be a JSON object in the following form, where the +`pattern` value may include comma-separated names and wildcards. + +```js +{ + pattern: "index-name-pattern" +} +``` + +The response body is a JSON object in the following form, where each element +of the `indices` array is the name of an index or alias. The number of elements +returned is limited, as this API is intended to be used to help narrow down +index names to use with the alert, and not support pagination, etc. + +```js +{ + indices: ["index-name-1", "alias-name-1", ...] +} +``` + +### `POST .../_fields` + +This HTTP endpoint is provided for the alerting ui to list the available +fields for the user to select to use with the alert. + +The request body is expected to be a JSON object in the following form, where the +`indexPatterns` array elements may include comma-separated names and wildcards. + +```js +{ + indexPatterns: ["index-pattern-1", "index-pattern-2"] +} +``` + +The response body is a JSON object in the following form, where each element +fields array is a field object. + +```js +{ + fields: [fieldObject1, fieldObject2, ...] +} +``` + +A field object is the following shape: + +```typescript +{ + name: string, // field name + type: string, // field type - eg 'keyword', 'date', 'long', etc + normalizedType: string, // for numeric types, this will be 'number' + aggregatable: true, // value from elasticsearch field capabilities + searchable: true, // value from elasticsearch field capabilities +} +``` + +### `POST .../_time_series_query` + +This HTTP endpoint is provided to return the values the alertType would calculate, +over a series of time. It is intended to be used in the alerting UI to +provide a "preview" of the alert during creation/editing based on recent data, +and could be used to show a "simulation" of the the alert over an arbitrary +range of time. + +The endpoint is `POST /api/triggers_actions_ui/data/_time_series_query`. +The request and response bodies are specifed in +[`lib/core_query_types.ts`][it-core-query] +and +[`lib/time_series_types.ts`][it-timeSeries-types]. +The request body is very similar to the alertType's parameters. + +### example + +Continuing with the example above, here's a query to get the values calculated +for the last 10 seconds. +This example uses [now-iso][] to generate iso date strings. + +```console +curl -k "https://elastic:changeme@localhost:5601/api/triggers_actions_ui/data/_time_series_query" \ + -H "kbn-xsrf: foo" -H "content-type: application/json" -d "{ + \"index\": \"es-hb-sim\", + \"timeField\": \"@timestamp\", + \"aggType\": \"avg\", + \"aggField\": \"summary.up\", + \"groupBy\": \"top\", + \"termSize\": 100, + \"termField\": \"monitor.name.keyword\", + \"interval\": \"1s\", + \"dateStart\": \"`now-iso -10s`\", + \"dateEnd\": \"`now-iso`\", + \"timeWindowSize\": 5, + \"timeWindowUnit\": \"s\" +}" +``` + +``` +{ + "results": [ + { + "group": "host-A", + "metrics": [ + [ "2020-02-26T15:10:40.000Z", 0 ], + [ "2020-02-26T15:10:41.000Z", 0 ], + [ "2020-02-26T15:10:42.000Z", 0 ], + [ "2020-02-26T15:10:43.000Z", 0 ], + [ "2020-02-26T15:10:44.000Z", 0 ], + [ "2020-02-26T15:10:45.000Z", 0 ], + [ "2020-02-26T15:10:46.000Z", 0 ], + [ "2020-02-26T15:10:47.000Z", 0 ], + [ "2020-02-26T15:10:48.000Z", 0 ], + [ "2020-02-26T15:10:49.000Z", 0 ], + [ "2020-02-26T15:10:50.000Z", 0 ] + ] + } + ] +} +``` + +To get the current value of the calculated metric, you can leave off the date: + +``` +curl -k "https://elastic:changeme@localhost:5601/api/triggers_actions_ui/data/_time_series_query" \ + -H "kbn-xsrf: foo" -H "content-type: application/json" -d '{ + "index": "es-hb-sim", + "timeField": "@timestamp", + "aggType": "avg", + "aggField": "summary.up", + "groupBy": "top", + "termField": "monitor.name.keyword", + "termSize": 100, + "interval": "1s", + "timeWindowSize": 5, + "timeWindowUnit": "s" +}' +``` + +``` +{ + "results": [ + { + "group": "host-A", + "metrics": [ + [ "2020-02-26T15:23:36.635Z", 0 ] + ] + } + ] +} +``` + +[it-timeSeries-types]: lib/time_series_types.ts + +## service functions + +A single service function is available that provides the functionality +of the http endpoint `POST /api/triggers_actions_ui/data/_time_series_query`, +but as an API for Kibana plugins. The function is available as +`triggersActionsUi.data.timeSeriesQuery()` on the plugin's _Start_ contract + +The parameters and return value for the function are the same as for the HTTP +request, though some additional parameters are required (logger, callCluster, +etc). + +## notes on the timeSeriesQuery API / http endpoint + +This API provides additional parameters beyond what the alertType itself uses: + +- `dateStart` +- `dateEnd` +- `interval` + +The `dateStart` and `dateEnd` parameters are ISO date strings. + +The `interval` parameter is intended to model the `interval` the alert is +currently using, and uses the same `1s`, `2m`, `3h`, etc format. Over the +supplied date range, a time-series data point will be calculated every +`interval` duration. + +So the number of time-series points in the output of the API should be: + +``` +( dateStart - dateEnd ) / interval +``` + +Example: + +``` +dateStart: '2020-01-01T00:00:00' +dateEnd: '2020-01-02T00:00:00' +interval: '1h' +``` + +The date range is 1 day === 24 hours. The interval is 1 hour. So there should +be ~24 time series points in the output. + +For preview purposes: + +- The `termSize` parameter should be used to help cut +down on the amount of work ES does, and keep the generated graphs a little +simpler. Probably something like `10`. + +- For queries with long date ranges, you probably don't want to use the +`interval` the alert is set to, as the `interval` used in the query, as this +could result in a lot of time-series points being generated, which is both +costly in ES, and may result in noisy graphs. + +- The `timeWindow*` parameters should be the same as what the alert is using, +especially for the `count` and `sum` aggregation types. Those aggregations +don't scale the same way the others do, when the window changes. Even for +the other aggregations, changing the window could result in dramatically +different values being generated - `avg` will be more "average-y", `min` +and `max` will be a little stickier. \ No newline at end of file diff --git a/x-pack/plugins/triggers_actions_ui/server/data/index.ts b/x-pack/plugins/triggers_actions_ui/server/data/index.ts new file mode 100644 index 00000000000000..6ee2b4bb8a5fe8 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/server/data/index.ts @@ -0,0 +1,40 @@ +/* + * 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, IRouter } from '../../../../../src/core/server'; +import { timeSeriesQuery } from './lib/time_series_query'; +import { registerRoutes } from './routes'; + +export { + TimeSeriesQuery, + CoreQueryParams, + CoreQueryParamsSchemaProperties, + validateCoreQueryBody, +} from './lib'; + +// future enhancement: make these configurable? +export const MAX_INTERVALS = 1000; +export const MAX_GROUPS = 1000; +export const DEFAULT_GROUPS = 100; + +export function getService() { + return { + timeSeriesQuery, + }; +} + +interface RegisterParams { + logger: Logger; + router: IRouter; + data: ReturnType; + baseRoute: string; +} + +export function register(params: RegisterParams) { + const { logger, router, data, baseRoute } = params; + const baseBuiltInRoute = `${baseRoute}/data`; + registerRoutes({ logger, router, data, baseRoute: baseBuiltInRoute }); +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/core_query_types.test.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/core_query_types.test.ts similarity index 100% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/core_query_types.test.ts rename to x-pack/plugins/triggers_actions_ui/server/data/lib/core_query_types.test.ts diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/core_query_types.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/core_query_types.ts similarity index 68% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/core_query_types.ts rename to x-pack/plugins/triggers_actions_ui/server/data/lib/core_query_types.ts index d96f555eceff45..bc7d0c352756e6 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/core_query_types.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/core_query_types.ts @@ -51,33 +51,45 @@ export function validateCoreQueryBody(anyParams: unknown): string | undefined { termSize, }: CoreQueryParams = anyParams as CoreQueryParams; if (aggType !== 'count' && !aggField) { - return i18n.translate('xpack.stackAlerts.indexThreshold.aggTypeRequiredErrorMessage', { - defaultMessage: '[aggField]: must have a value when [aggType] is "{aggType}"', - values: { - aggType, - }, - }); + return i18n.translate( + 'xpack.triggersActionsUI.data.coreQueryParams.aggTypeRequiredErrorMessage', + { + defaultMessage: '[aggField]: must have a value when [aggType] is "{aggType}"', + values: { + aggType, + }, + } + ); } // check grouping if (groupBy === 'top') { if (termField == null) { - return i18n.translate('xpack.stackAlerts.indexThreshold.termFieldRequiredErrorMessage', { - defaultMessage: '[termField]: termField required when [groupBy] is top', - }); + return i18n.translate( + 'xpack.triggersActionsUI.data.coreQueryParams.termFieldRequiredErrorMessage', + { + defaultMessage: '[termField]: termField required when [groupBy] is top', + } + ); } if (termSize == null) { - return i18n.translate('xpack.stackAlerts.indexThreshold.termSizeRequiredErrorMessage', { - defaultMessage: '[termSize]: termSize required when [groupBy] is top', - }); + return i18n.translate( + 'xpack.triggersActionsUI.data.coreQueryParams.termSizeRequiredErrorMessage', + { + defaultMessage: '[termSize]: termSize required when [groupBy] is top', + } + ); } if (termSize > MAX_GROUPS) { - return i18n.translate('xpack.stackAlerts.indexThreshold.invalidTermSizeMaximumErrorMessage', { - defaultMessage: '[termSize]: must be less than or equal to {maxGroups}', - values: { - maxGroups: MAX_GROUPS, - }, - }); + return i18n.translate( + 'xpack.triggersActionsUI.data.coreQueryParams.invalidTermSizeMaximumErrorMessage', + { + defaultMessage: '[termSize]: must be less than or equal to {maxGroups}', + values: { + maxGroups: MAX_GROUPS, + }, + } + ); } } } @@ -89,7 +101,7 @@ function validateAggType(aggType: string): string | undefined { return; } - return i18n.translate('xpack.stackAlerts.indexThreshold.invalidAggTypeErrorMessage', { + return i18n.translate('xpack.triggersActionsUI.data.coreQueryParams.invalidAggTypeErrorMessage', { defaultMessage: 'invalid aggType: "{aggType}"', values: { aggType, @@ -102,7 +114,7 @@ export function validateGroupBy(groupBy: string): string | undefined { return; } - return i18n.translate('xpack.stackAlerts.indexThreshold.invalidGroupByErrorMessage', { + return i18n.translate('xpack.triggersActionsUI.data.coreQueryParams.invalidGroupByErrorMessage', { defaultMessage: 'invalid groupBy: "{groupBy}"', values: { groupBy, @@ -117,10 +129,13 @@ export function validateTimeWindowUnits(timeWindowUnit: string): string | undefi return; } - return i18n.translate('xpack.stackAlerts.indexThreshold.invalidTimeWindowUnitsErrorMessage', { - defaultMessage: 'invalid timeWindowUnit: "{timeWindowUnit}"', - values: { - timeWindowUnit, - }, - }); + return i18n.translate( + 'xpack.triggersActionsUI.data.coreQueryParams.invalidTimeWindowUnitsErrorMessage', + { + defaultMessage: 'invalid timeWindowUnit: "{timeWindowUnit}"', + values: { + timeWindowUnit, + }, + } + ); } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/date_range_info.test.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/date_range_info.test.ts similarity index 100% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/date_range_info.test.ts rename to x-pack/plugins/triggers_actions_ui/server/data/lib/date_range_info.test.ts diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/date_range_info.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/date_range_info.ts similarity index 89% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/date_range_info.ts rename to x-pack/plugins/triggers_actions_ui/server/data/lib/date_range_info.ts index 34f276d08706ba..da125ba7ea29dd 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/date_range_info.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/date_range_info.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { times } from 'lodash'; -import { parseDuration } from '../../../../../alerts/server'; +import { parseDuration } from '../../../../alerts/server'; import { MAX_INTERVALS } from '../index'; // dates as numbers are epoch millis @@ -100,7 +100,7 @@ function getDuration(durationS: string, field: string): number { } function getParseErrorMessage(formatName: string, fieldName: string, fieldValue: string) { - return i18n.translate('xpack.stackAlerts.indexThreshold.formattedFieldErrorMessage', { + return i18n.translate('xpack.triggersActionsUI.data.coreQueryParams.formattedFieldErrorMessage', { defaultMessage: 'invalid {formatName} format for {fieldName}: "{fieldValue}"', values: { formatName, @@ -111,7 +111,7 @@ function getParseErrorMessage(formatName: string, fieldName: string, fieldValue: } export function getTooManyIntervalsErrorMessage(intervals: number, maxIntervals: number) { - return i18n.translate('xpack.stackAlerts.indexThreshold.maxIntervalsErrorMessage', { + return i18n.translate('xpack.triggersActionsUI.data.coreQueryParams.maxIntervalsErrorMessage', { defaultMessage: 'calculated number of intervals {intervals} is greater than maximum {maxIntervals}', values: { @@ -122,7 +122,10 @@ export function getTooManyIntervalsErrorMessage(intervals: number, maxIntervals: } export function getDateStartAfterDateEndErrorMessage(): string { - return i18n.translate('xpack.stackAlerts.indexThreshold.dateStartGTdateEndErrorMessage', { - defaultMessage: '[dateStart]: is greater than [dateEnd]', - }); + return i18n.translate( + 'xpack.triggersActionsUI.data.coreQueryParams.dateStartGTdateEndErrorMessage', + { + defaultMessage: '[dateStart]: is greater than [dateEnd]', + } + ); } diff --git a/x-pack/plugins/triggers_actions_ui/server/data/lib/index.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/index.ts new file mode 100644 index 00000000000000..096a928249fd5a --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { TimeSeriesQuery } from './time_series_query'; +export { + CoreQueryParams, + CoreQueryParamsSchemaProperties, + validateCoreQueryBody, +} from './core_query_types'; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_query.test.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.test.ts similarity index 58% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_query.test.ts rename to x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.test.ts index 0565a8634fc71c..f1234249a257f6 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_query.test.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.test.ts @@ -6,12 +6,8 @@ // test error conditions of calling timeSeriesQuery - postive results tested in FT -import { loggingSystemMock } from '../../../../../../../src/core/server/mocks'; -import { coreMock } from '../../../../../../../src/core/server/mocks'; -import { AlertingBuiltinsPlugin } from '../../../plugin'; -import { TimeSeriesQueryParameters, TimeSeriesResult, TimeSeriesQuery } from './time_series_query'; - -type TimeSeriesQueryFn = (query: TimeSeriesQueryParameters) => Promise; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { TimeSeriesQueryParameters, TimeSeriesQuery, timeSeriesQuery } from './time_series_query'; const DefaultQueryParams: TimeSeriesQuery = { index: 'index-name', @@ -32,16 +28,7 @@ describe('timeSeriesQuery', () => { let params: TimeSeriesQueryParameters; const mockCallCluster = jest.fn(); - let timeSeriesQueryFn: TimeSeriesQueryFn; - beforeEach(async () => { - // rather than use the function from an import, retrieve it from the plugin - const context = coreMock.createPluginInitializerContext(); - const plugin = new AlertingBuiltinsPlugin(context); - const coreStart = coreMock.createStart(); - const service = await plugin.start(coreStart); - timeSeriesQueryFn = service.indexThreshold.timeSeriesQuery; - mockCallCluster.mockReset(); params = { logger: loggingSystemMock.create().get(), @@ -52,14 +39,14 @@ describe('timeSeriesQuery', () => { it('fails as expected when the callCluster call fails', async () => { mockCallCluster.mockRejectedValue(new Error('woopsie')); - expect(timeSeriesQueryFn(params)).rejects.toThrowErrorMatchingInlineSnapshot( + expect(timeSeriesQuery(params)).rejects.toThrowErrorMatchingInlineSnapshot( `"error running search"` ); }); it('fails as expected when the query params are invalid', async () => { params.query = { ...params.query, dateStart: 'x' }; - expect(timeSeriesQueryFn(params)).rejects.toThrowErrorMatchingInlineSnapshot( + expect(timeSeriesQuery(params)).rejects.toThrowErrorMatchingInlineSnapshot( `"invalid date format for dateStart: \\"x\\""` ); }); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_query.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts similarity index 96% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_query.ts rename to x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts index 9c4133be6f4837..29a996bbb5ef66 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_query.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts @@ -5,16 +5,17 @@ */ import { SearchResponse } from 'elasticsearch'; +import { Logger } from 'kibana/server'; +import { LegacyScopedClusterClient } from '../../../../../../src/core/server'; import { DEFAULT_GROUPS } from '../index'; import { getDateRangeInfo } from './date_range_info'; -import { Logger, CallCluster } from '../../../types'; import { TimeSeriesQuery, TimeSeriesResult, TimeSeriesResultRow } from './time_series_types'; export { TimeSeriesQuery, TimeSeriesResult } from './time_series_types'; export interface TimeSeriesQueryParameters { logger: Logger; - callCluster: CallCluster; + callCluster: LegacyScopedClusterClient['callAsCurrentUser']; query: TimeSeriesQuery; } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_types.test.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_types.test.ts similarity index 100% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_types.test.ts rename to x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_types.test.ts diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_types.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_types.ts similarity index 81% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_types.ts rename to x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_types.ts index a8b35c34c596fd..ef0fa15cf31e92 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_types.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_types.ts @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; -import { parseDuration } from '../../../../../alerts/server'; +import { parseDuration } from '../../../../alerts/server'; import { MAX_INTERVALS } from '../index'; import { CoreQueryParamsSchemaProperties, validateCoreQueryBody } from './core_query_types'; import { @@ -18,11 +18,7 @@ import { getDateStartAfterDateEndErrorMessage, } from './date_range_info'; -export { - TimeSeriesResult, - TimeSeriesResultRow, - MetricResult, -} from '../../../../common/alert_types/index_threshold'; +export { TimeSeriesResult, TimeSeriesResultRow, MetricResult } from '../../../common/data'; // The parameters here are very similar to the alert parameters. // Missing are `comparator` and `threshold`, which aren't needed to generate @@ -66,9 +62,12 @@ function validateBody(anyParams: unknown): string | undefined { } if (epochStart !== epochEnd && !interval) { - return i18n.translate('xpack.stackAlerts.indexThreshold.intervalRequiredErrorMessage', { - defaultMessage: '[interval]: must be specified if [dateStart] does not equal [dateEnd]', - }); + return i18n.translate( + 'xpack.triggersActionsUI.data.coreQueryParams.intervalRequiredErrorMessage', + { + defaultMessage: '[interval]: must be specified if [dateStart] does not equal [dateEnd]', + } + ); } if (interval) { @@ -84,7 +83,7 @@ function validateBody(anyParams: unknown): string | undefined { function validateDate(dateString: string): string | undefined { const parsed = Date.parse(dateString); if (isNaN(parsed)) { - return i18n.translate('xpack.stackAlerts.indexThreshold.invalidDateErrorMessage', { + return i18n.translate('xpack.triggersActionsUI.data.coreQueryParams.invalidDateErrorMessage', { defaultMessage: 'invalid date {date}', values: { date: dateString, @@ -97,11 +96,14 @@ export function validateDuration(duration: string): string | undefined { try { parseDuration(duration); } catch (err) { - return i18n.translate('xpack.stackAlerts.indexThreshold.invalidDurationErrorMessage', { - defaultMessage: 'invalid duration: "{duration}"', - values: { - duration, - }, - }); + return i18n.translate( + 'xpack.triggersActionsUI.data.coreQueryParams.invalidDurationErrorMessage', + { + defaultMessage: 'invalid duration: "{duration}"', + values: { + duration, + }, + } + ); } } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/routes/fields.ts b/x-pack/plugins/triggers_actions_ui/server/data/routes/fields.ts similarity index 90% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/routes/fields.ts rename to x-pack/plugins/triggers_actions_ui/server/data/routes/fields.ts index ea1e17002c4a5c..17a2b2929f0cf4 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/routes/fields.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/routes/fields.ts @@ -16,7 +16,7 @@ import { KibanaResponseFactory, ILegacyScopedClusterClient, } from 'kibana/server'; -import { Service } from '../../../types'; +import { Logger } from '../../../../../../src/core/server'; const bodySchema = schema.object({ indexPatterns: schema.arrayOf(schema.string()), @@ -24,9 +24,9 @@ const bodySchema = schema.object({ type RequestBody = TypeOf; -export function createFieldsRoute(service: Service, router: IRouter, baseRoute: string) { +export function createFieldsRoute(logger: Logger, router: IRouter, baseRoute: string) { const path = `${baseRoute}/_fields`; - service.logger.debug(`registering indexThreshold route POST ${path}`); + logger.debug(`registering indexThreshold route POST ${path}`); router.post( { path, @@ -41,7 +41,7 @@ export function createFieldsRoute(service: Service, router: IRouter, baseRoute: req: KibanaRequest, res: KibanaResponseFactory ): Promise { - service.logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`); + logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`); let rawFields: RawFields; @@ -54,7 +54,7 @@ export function createFieldsRoute(service: Service, router: IRouter, baseRoute: rawFields = await getRawFields(ctx.core.elasticsearch.legacy.client, req.body.indexPatterns); } catch (err) { const indexPatterns = req.body.indexPatterns.join(','); - service.logger.warn( + logger.warn( `route ${path} error getting fields from pattern "${indexPatterns}": ${err.message}` ); return res.ok({ body: { fields: [] } }); @@ -62,7 +62,7 @@ export function createFieldsRoute(service: Service, router: IRouter, baseRoute: const result = { fields: getFieldsFromRawFields(rawFields) }; - service.logger.debug(`route ${path} response: ${JSON.stringify(result)}`); + logger.debug(`route ${path} response: ${JSON.stringify(result)}`); return res.ok({ body: result }); } } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/routes/index.ts b/x-pack/plugins/triggers_actions_ui/server/data/routes/index.ts similarity index 55% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/routes/index.ts rename to x-pack/plugins/triggers_actions_ui/server/data/routes/index.ts index 8410e48dd46d9d..664b78cabb560b 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/routes/index.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/routes/index.ts @@ -4,19 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Service, IRouter } from '../../../types'; +import { Logger } from '../../../../../../src/core/server'; import { createTimeSeriesQueryRoute } from './time_series_query'; import { createFieldsRoute } from './fields'; import { createIndicesRoute } from './indices'; +import { IRouter } from '../../../../../../src/core/server'; +import { getService } from '..'; interface RegisterRoutesParams { - service: Service; + logger: Logger; router: IRouter; + data: ReturnType; baseRoute: string; } export function registerRoutes(params: RegisterRoutesParams) { - const { service, router, baseRoute } = params; - createTimeSeriesQueryRoute(service, router, baseRoute); - createFieldsRoute(service, router, baseRoute); - createIndicesRoute(service, router, baseRoute); + const { logger, router, baseRoute, data } = params; + createTimeSeriesQueryRoute(logger, data.timeSeriesQuery, router, baseRoute); + createFieldsRoute(logger, router, baseRoute); + createIndicesRoute(logger, router, baseRoute); } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/routes/indices.ts b/x-pack/plugins/triggers_actions_ui/server/data/routes/indices.ts similarity index 85% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/routes/indices.ts rename to x-pack/plugins/triggers_actions_ui/server/data/routes/indices.ts index c94705829ec602..9b84ce5ac0bcc6 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/routes/indices.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/routes/indices.ts @@ -19,7 +19,7 @@ import { ILegacyScopedClusterClient, } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; -import { Service } from '../../../types'; +import { Logger } from '../../../../../../src/core/server'; const bodySchema = schema.object({ pattern: schema.string(), @@ -27,9 +27,9 @@ const bodySchema = schema.object({ type RequestBody = TypeOf; -export function createIndicesRoute(service: Service, router: IRouter, baseRoute: string) { +export function createIndicesRoute(logger: Logger, router: IRouter, baseRoute: string) { const path = `${baseRoute}/_indices`; - service.logger.debug(`registering indexThreshold route POST ${path}`); + logger.debug(`registering indexThreshold route POST ${path}`); router.post( { path, @@ -45,7 +45,7 @@ export function createIndicesRoute(service: Service, router: IRouter, baseRoute: res: KibanaResponseFactory ): Promise { const pattern = req.body.pattern; - service.logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`); + logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`); if (pattern.trim() === '') { return res.ok({ body: { indices: [] } }); @@ -55,23 +55,19 @@ export function createIndicesRoute(service: Service, router: IRouter, baseRoute: try { aliases = await getAliasesFromPattern(ctx.core.elasticsearch.legacy.client, pattern); } catch (err) { - service.logger.warn( - `route ${path} error getting aliases from pattern "${pattern}": ${err.message}` - ); + logger.warn(`route ${path} error getting aliases from pattern "${pattern}": ${err.message}`); } let indices: string[] = []; try { indices = await getIndicesFromPattern(ctx.core.elasticsearch.legacy.client, pattern); } catch (err) { - service.logger.warn( - `route ${path} error getting indices from pattern "${pattern}": ${err.message}` - ); + logger.warn(`route ${path} error getting indices from pattern "${pattern}": ${err.message}`); } const result = { indices: uniqueCombined(aliases, indices, MAX_INDICES) }; - service.logger.debug(`route ${path} response: ${JSON.stringify(result)}`); + logger.debug(`route ${path} response: ${JSON.stringify(result)}`); return res.ok({ body: result }); } } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/routes/time_series_query.ts b/x-pack/plugins/triggers_actions_ui/server/data/routes/time_series_query.ts similarity index 58% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/routes/time_series_query.ts rename to x-pack/plugins/triggers_actions_ui/server/data/routes/time_series_query.ts index 9af01dc766a99d..805bd7d4004c20 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/routes/time_series_query.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/routes/time_series_query.ts @@ -11,14 +11,20 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; +import { Logger } from '../../../../../../src/core/server'; +import { TimeSeriesQueryParameters } from '../lib/time_series_query'; -import { Service } from '../../../types'; -import { TimeSeriesQuery, TimeSeriesQuerySchema } from '../lib/time_series_types'; +import { TimeSeriesQuery, TimeSeriesQuerySchema, TimeSeriesResult } from '../lib/time_series_types'; export { TimeSeriesQuery, TimeSeriesResult } from '../lib/time_series_types'; -export function createTimeSeriesQueryRoute(service: Service, router: IRouter, baseRoute: string) { +export function createTimeSeriesQueryRoute( + logger: Logger, + timeSeriesQuery: (params: TimeSeriesQueryParameters) => Promise, + router: IRouter, + baseRoute: string +) { const path = `${baseRoute}/_time_series_query`; - service.logger.debug(`registering indexThreshold route POST ${path}`); + logger.debug(`registering indexThreshold route POST ${path}`); router.post( { path, @@ -33,15 +39,15 @@ export function createTimeSeriesQueryRoute(service: Service, router: IRouter, ba req: KibanaRequest, res: KibanaResponseFactory ): Promise { - service.logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`); + logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`); - const result = await service.indexThreshold.timeSeriesQuery({ - logger: service.logger, + const result = await timeSeriesQuery({ + logger, callCluster: ctx.core.elasticsearch.legacy.client.callAsCurrentUser, query: req.body, }); - service.logger.debug(`route ${path} response: ${JSON.stringify(result)}`); + logger.debug(`route ${path} response: ${JSON.stringify(result)}`); return res.ok({ body: result }); } } diff --git a/x-pack/plugins/triggers_actions_ui/server/index.ts b/x-pack/plugins/triggers_actions_ui/server/index.ts index c12572f4ea7e93..abd61f2bd3541f 100644 --- a/x-pack/plugins/triggers_actions_ui/server/index.ts +++ b/x-pack/plugins/triggers_actions_ui/server/index.ts @@ -4,8 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginConfigDescriptor } from 'kibana/server'; +import { PluginConfigDescriptor, PluginInitializerContext } from 'kibana/server'; import { configSchema, ConfigSchema } from '../config'; +import { TriggersActionsPlugin } from './plugin'; + +export { PluginStartContract } from './plugin'; +export { + TimeSeriesQuery, + CoreQueryParams, + CoreQueryParamsSchemaProperties, + validateCoreQueryBody, + MAX_INTERVALS, + MAX_GROUPS, + DEFAULT_GROUPS, +} from './data'; export const config: PluginConfigDescriptor = { exposeToBrowser: { @@ -14,7 +26,4 @@ export const config: PluginConfigDescriptor = { schema: configSchema, }; -export const plugin = () => ({ - setup() {}, - start() {}, -}); +export const plugin = (ctx: PluginInitializerContext) => new TriggersActionsPlugin(ctx); diff --git a/x-pack/plugins/triggers_actions_ui/server/plugin.ts b/x-pack/plugins/triggers_actions_ui/server/plugin.ts new file mode 100644 index 00000000000000..c0d29341e217bb --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/server/plugin.ts @@ -0,0 +1,39 @@ +/* + * 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, Plugin, CoreSetup, PluginInitializerContext } from 'src/core/server'; +import { getService, register as registerDataService } from './data'; + +export interface PluginStartContract { + data: ReturnType; +} + +export class TriggersActionsPlugin implements Plugin { + private readonly logger: Logger; + private readonly data: PluginStartContract['data']; + + constructor(ctx: PluginInitializerContext) { + this.logger = ctx.logger.get(); + this.data = getService(); + } + + public async setup(core: CoreSetup): Promise { + registerDataService({ + logger: this.logger, + data: this.data, + router: core.http.createRouter(), + baseRoute: '/api/triggers_actions_ui', + }); + } + + public async start(): Promise { + return { + data: this.data, + }; + } + + public async stop(): Promise {} +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts index c05fa6cf051ff6..e918ce174a031f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts @@ -15,7 +15,6 @@ import { ObjectRemover, } from '../../../../../common/lib'; import { createEsDocuments } from './create_test_data'; -import { getAlertType } from '../../../../../../../plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/'; const ALERT_TYPE_ID = '.index-threshold'; const ACTION_TYPE_ID = '.index'; @@ -27,7 +26,7 @@ const ALERT_INTERVALS_TO_WRITE = 5; const ALERT_INTERVAL_SECONDS = 3; const ALERT_INTERVAL_MILLIS = ALERT_INTERVAL_SECONDS * 1000; -const DefaultActionMessage = getAlertType().defaultActionMessage; +const DefaultActionMessage = `alert {{alertName}} group {{context.group}} value {{context.value}} exceeded threshold {{context.function}} over {{params.timeWindowSize}}{{params.timeWindowUnit}} on {{context.date}}`; // eslint-disable-next-line import/no-default-export export default function alertTests({ getService }: FtrProviderContext) { @@ -65,10 +64,6 @@ export default function alertTests({ getService }: FtrProviderContext) { await esTestIndexToolOutput.destroy(); }); - it('has a default action message', () => { - expect(DefaultActionMessage).to.be.ok(); - }); - // The tests below create two alerts, one that will fire, one that will // never fire; the tests ensure the ones that should fire, do fire, and // those that shouldn't fire, do not fire. diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/fields_endpoint.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/fields_endpoint.ts index 76ff41aac5397a..881be83236be55 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/fields_endpoint.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/fields_endpoint.ts @@ -10,7 +10,7 @@ import { Spaces } from '../../../../scenarios'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { ESTestIndexTool, ES_TEST_INDEX_NAME, getUrlPrefix } from '../../../../../common/lib'; -const API_URI = 'api/stack_alerts/index_threshold/_fields'; +const API_URI = 'api/triggers_actions_ui/data/_fields'; // eslint-disable-next-line import/no-default-export export default function fieldsEndpointTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/indices_endpoint.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/indices_endpoint.ts index ba2b71e7134b61..7d89e2701d6282 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/indices_endpoint.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/indices_endpoint.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { ESTestIndexTool, ES_TEST_INDEX_NAME, getUrlPrefix } from '../../../../../common/lib'; import { createEsDocuments } from './create_test_data'; -const API_URI = 'api/stack_alerts/index_threshold/_indices'; +const API_URI = 'api/triggers_actions_ui/data/_indices'; // eslint-disable-next-line import/no-default-export export default function indicesEndpointTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts index 70dbb860e08d68..334f898232bbcf 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts @@ -9,11 +9,11 @@ import expect from '@kbn/expect'; import { Spaces } from '../../../../scenarios'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { ESTestIndexTool, ES_TEST_INDEX_NAME, getUrlPrefix } from '../../../../../common/lib'; -import { TimeSeriesQuery } from '../../../../../../../plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_query'; +import { TimeSeriesQuery } from '../../../../../../../plugins/triggers_actions_ui/server'; import { createEsDocuments } from './create_test_data'; -const INDEX_THRESHOLD_TIME_SERIES_QUERY_URL = 'api/stack_alerts/index_threshold/_time_series_query'; +const INDEX_THRESHOLD_TIME_SERIES_QUERY_URL = 'api/triggers_actions_ui/data/_time_series_query'; const START_DATE_MM_DD_HH_MM_SS_MS = '01-01T00:00:00.000Z'; const START_DATE = `2020-${START_DATE_MM_DD_HH_MM_SS_MS}`; From 1babb5f6bfb08791506c5472377c2d76c3ac0dc8 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Thu, 12 Nov 2020 11:44:15 -0500 Subject: [PATCH 30/52] [Fleet] IngestManager Plugin interface for registering UI extensions (#82783) * Expose `registerExtension()` interface on `Plugin#start` * Refactor use of `CustomConfigurePackagePolicy` to the new registerExtension approach * Refactor to always show registered ui extension (even if Integration has configuration options) --- .../fleet/components/extension_wrapper.tsx | 17 ++++ .../fleet/hooks/use_ui_extension.ts | 35 +++++++ .../fleet/public/applications/fleet/index.tsx | 12 ++- .../components/custom_package_policy.tsx | 61 ------------ .../components/index.ts | 1 - .../create_package_policy_page/index.tsx | 34 ++++++- .../step_configure_package.tsx | 38 ++++++-- .../edit_package_policy_page/index.tsx | 48 +++++++++- .../fleet/services/ui_extensions.test.ts | 76 +++++++++++++++ .../fleet/services/ui_extensions.ts | 26 +++++ .../public/applications/fleet/types/index.ts | 2 + .../applications/fleet/types/ui_extensions.ts | 96 +++++++++++++++++++ x-pack/plugins/fleet/public/index.ts | 7 +- x-pack/plugins/fleet/public/plugin.ts | 20 +++- .../mock/endpoint/dependencies_start_mock.ts | 4 +- .../endpoint_policy_create_extension.tsx | 35 +++++++ ...tsx => endpoint_policy_edit_extension.tsx} | 34 ++----- .../lazy_endpoint_policy_create_extension.tsx | 17 ++++ .../lazy_endpoint_policy_edit_extension.tsx | 18 ++++ .../security_solution/public/plugin.tsx | 22 +++-- 20 files changed, 478 insertions(+), 125 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/components/extension_wrapper.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/hooks/use_ui_extension.ts delete mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/custom_package_policy.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/services/ui_extensions.test.ts create mode 100644 x-pack/plugins/fleet/public/applications/fleet/services/ui_extensions.ts create mode 100644 x-pack/plugins/fleet/public/applications/fleet/types/ui_extensions.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension.tsx rename x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/{configure_package_policy.tsx => endpoint_policy_edit_extension.tsx} (81%) create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension.tsx diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/extension_wrapper.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/extension_wrapper.tsx new file mode 100644 index 00000000000000..874c91e8e546b1 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/components/extension_wrapper.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, ReactNode, Suspense } from 'react'; +import { EuiErrorBoundary } from '@elastic/eui'; +import { Loading } from './loading'; + +export const ExtensionWrapper = memo<{ children: ReactNode }>(({ children }) => { + return ( + + }>{children} + + ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_ui_extension.ts b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_ui_extension.ts new file mode 100644 index 00000000000000..93bc1eae28cf6c --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_ui_extension.ts @@ -0,0 +1,35 @@ +/* + * 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 { UIExtensionPoint, UIExtensionsStorage } from '../types'; + +export const UIExtensionsContext = React.createContext({}); + +type NarrowExtensionPoint = A extends { + view: V; +} + ? A + : never; + +export const useUIExtension = ( + packageName: UIExtensionPoint['package'], + view: V +): NarrowExtensionPoint['component'] | undefined => { + const registeredExtensions = useContext(UIExtensionsContext); + + if (!registeredExtensions) { + throw new Error('useUIExtension called outside of UIExtensionsContext'); + } + + const extension = registeredExtensions?.[packageName]?.[view]; + + if (extension) { + // FIXME:PT Revisit ignore below and see if TS error can be addressed + // @ts-ignore + return extension.component; + } +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/index.tsx index a49306f8e8d554..d4e652ad95831b 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/index.tsx @@ -36,6 +36,8 @@ import { import { PackageInstallProvider } from './sections/epm/hooks'; import { FleetStatusProvider, useBreadcrumbs } from './hooks'; import { IntraAppStateProvider } from './hooks/use_intra_app_state'; +import { UIExtensionsStorage } from './types'; +import { UIExtensionsContext } from './hooks/use_ui_extension'; export interface ProtectedRouteProps extends RouteProps { isAllowed?: boolean; @@ -235,6 +237,7 @@ const IngestManagerApp = ({ config, history, kibanaVersion, + extensions, }: { basepath: string; coreStart: CoreStart; @@ -243,6 +246,7 @@ const IngestManagerApp = ({ config: IngestManagerConfigType; history: AppMountParameters['history']; kibanaVersion: string; + extensions: UIExtensionsStorage; }) => { const isDarkMode = useObservable(coreStart.uiSettings.get$('theme:darkMode')); return ( @@ -252,7 +256,9 @@ const IngestManagerApp = ({ - + + + @@ -268,7 +274,8 @@ export function renderApp( setupDeps: IngestManagerSetupDeps, startDeps: IngestManagerStartDeps, config: IngestManagerConfigType, - kibanaVersion: string + kibanaVersion: string, + extensions: UIExtensionsStorage ) { ReactDOM.render( , element ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/custom_package_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/custom_package_policy.tsx deleted file mode 100644 index d5163e1b9abbed..00000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/custom_package_policy.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiEmptyPrompt, EuiText } from '@elastic/eui'; -import { NewPackagePolicy } from '../../../../types'; -import { CreatePackagePolicyFrom } from '../types'; - -export interface CustomConfigurePackagePolicyProps { - packageName: string; - from: CreatePackagePolicyFrom; - packagePolicy: NewPackagePolicy; - packagePolicyId?: string; -} - -/** - * Custom content type that external plugins can provide to Ingest's - * package policy UI. - */ -export type CustomConfigurePackagePolicyContent = React.FC; - -type AllowedPackageKey = 'endpoint'; -const PackagePolicyMapping: { - [key: string]: CustomConfigurePackagePolicyContent; -} = {}; - -/** - * Plugins can call this function from the start lifecycle to - * register a custom component in the Ingest package policy. - */ -export function registerPackagePolicyComponent( - key: AllowedPackageKey, - value: CustomConfigurePackagePolicyContent -) { - PackagePolicyMapping[key] = value; -} - -const EmptyPackagePolicy: CustomConfigurePackagePolicyContent = () => ( - -

- -

- - } - /> -); - -export const CustomPackagePolicy = (props: CustomConfigurePackagePolicyProps) => { - const CustomPackagePolicyContent = PackagePolicyMapping[props.packageName] || EmptyPackagePolicy; - return ; -}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/index.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/index.ts index 893ed00cca9ac3..58b5b1cd3126e9 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/index.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/index.ts @@ -6,4 +6,3 @@ export { CreatePackagePolicyPageLayout } from './layout'; export { PackagePolicyInputPanel } from './package_policy_input_panel'; export { PackagePolicyInputVarField } from './package_policy_input_var_field'; -export { CustomPackagePolicy } from './custom_package_policy'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx index b45794b9f87db3..a837ed33e41106 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx @@ -46,6 +46,9 @@ import { StepSelectAgentPolicy } from './step_select_agent_policy'; import { StepConfigurePackagePolicy } from './step_configure_package'; import { StepDefinePackagePolicy } from './step_define_package_policy'; import { useIntraAppState } from '../../../hooks/use_intra_app_state'; +import { useUIExtension } from '../../../hooks/use_ui_extension'; +import { ExtensionWrapper } from '../../../components/extension_wrapper'; +import { PackagePolicyEditExtensionComponentProps } from '../../../types'; const StepsWithLessPadding = styled(EuiSteps)` .euiStep__content { @@ -191,6 +194,21 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { [packagePolicy, updatePackagePolicyValidation] ); + const handleExtensionViewOnChange = useCallback< + PackagePolicyEditExtensionComponentProps['onChange'] + >( + ({ isValid, updatedPolicy }) => { + updatePackagePolicy(updatedPolicy); + setFormState((prevState) => { + if (prevState === 'VALID' && !isValid) { + return 'INVALID'; + } + return prevState; + }); + }, + [updatePackagePolicy] + ); + // Cancel path const cancelUrl = useMemo(() => { if (routeState && routeState.onCancelUrl) { @@ -287,6 +305,8 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { [pkgkey, updatePackageInfo, agentPolicy, updateAgentPolicy] ); + const ExtensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-create'); + const stepSelectPackage = useMemo( () => ( { validationResults={validationResults!} submitAttempted={formState === 'INVALID'} /> + {/* If an Agent Policy and a package has been selected, then show UI extension (if any) */} + {packagePolicy.policy_id && packagePolicy.package?.name && ExtensionView && ( + + + + )} ) : (
), [ - agentPolicy, - formState, isLoadingSecondStep, - packagePolicy, + agentPolicy, packageInfo, + packagePolicy, updatePackagePolicy, validationResults, + formState, + ExtensionView, + handleExtensionViewOnChange, ] ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.tsx index b335ff439684bb..671bc829af82a8 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.tsx @@ -4,7 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + EuiHorizontalRule, + EuiFlexGroup, + EuiFlexItem, + EuiEmptyPrompt, + EuiText, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { PackageInfo, RegistryStream, @@ -13,8 +20,9 @@ import { } from '../../../types'; import { Loading } from '../../../components'; import { PackagePolicyValidationResults } from './services'; -import { PackagePolicyInputPanel, CustomPackagePolicy } from './components'; +import { PackagePolicyInputPanel } from './components'; import { CreatePackagePolicyFrom } from './types'; +import { useUIExtension } from '../../../hooks/use_ui_extension'; const findStreamsForInputType = ( inputType: string, @@ -55,6 +63,12 @@ export const StepConfigurePackagePolicy: React.FunctionComponent<{ validationResults, submitAttempted, }) => { + const hasUiExtension = + useUIExtension( + packageInfo.name, + from === 'edit' ? 'package-policy-edit' : 'package-policy-create' + ) !== undefined; + // Configure inputs (and their streams) // Assume packages only export one config template for now const renderConfigureInputs = () => @@ -98,12 +112,20 @@ export const StepConfigurePackagePolicy: React.FunctionComponent<{ })} - ) : ( - +

+ +

+ + } /> ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index d642619515a57c..bfc10848d378f9 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -41,6 +41,10 @@ import { } from '../create_package_policy_page/types'; import { StepConfigurePackagePolicy } from '../create_package_policy_page/step_configure_package'; import { StepDefinePackagePolicy } from '../create_package_policy_page/step_define_package_policy'; +import { useUIExtension } from '../../../hooks/use_ui_extension'; +import { ExtensionWrapper } from '../../../components/extension_wrapper'; +import { GetOnePackagePolicyResponse } from '../../../../../../common/types/rest_spec'; +import { PackagePolicyEditExtensionComponentProps } from '../../../types'; export const EditPackagePolicyPage: React.FunctionComponent = () => { const { notifications } = useCore(); @@ -68,6 +72,9 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { inputs: [], version: '', }); + const [originalPackagePolicy, setOriginalPackagePolicy] = useState< + GetOnePackagePolicyResponse['item'] + >(); // Retrieve agent policy, package, and package policy info useEffect(() => { @@ -83,6 +90,8 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { setAgentPolicy(agentPolicyData.item); } if (packagePolicyData?.item) { + setOriginalPackagePolicy(packagePolicyData.item); + const { id, revision, @@ -189,6 +198,21 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { [packagePolicy, updatePackagePolicyValidation] ); + const handleExtensionViewOnChange = useCallback< + PackagePolicyEditExtensionComponentProps['onChange'] + >( + ({ isValid, updatedPolicy }) => { + updatePackagePolicy(updatedPolicy); + setFormState((prevState) => { + if (prevState === 'VALID' && !isValid) { + return 'INVALID'; + } + return prevState; + }); + }, + [updatePackagePolicy] + ); + // Cancel url const cancelUrl = getHref('policy_details', { policyId }); @@ -267,6 +291,8 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { packageInfo, }; + const ExtensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-edit'); + const configurePackage = useMemo( () => agentPolicy && packageInfo ? ( @@ -288,16 +314,32 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { validationResults={validationResults!} submitAttempted={formState === 'INVALID'} /> + + {packagePolicy.policy_id && + packagePolicy.package?.name && + originalPackagePolicy && + ExtensionView && ( + + + + )} ) : null, [ agentPolicy, - formState, - packagePolicy, - packagePolicyId, packageInfo, + packagePolicy, updatePackagePolicy, validationResults, + packagePolicyId, + formState, + originalPackagePolicy, + ExtensionView, + handleExtensionViewOnChange, ] ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/services/ui_extensions.test.ts b/x-pack/plugins/fleet/public/applications/fleet/services/ui_extensions.test.ts new file mode 100644 index 00000000000000..97c0203fab0563 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/services/ui_extensions.test.ts @@ -0,0 +1,76 @@ +/* + * 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 { lazy } from 'react'; + +import { + PackagePolicyEditExtensionComponent, + UIExtensionRegistrationCallback, + UIExtensionsStorage, +} from '../types'; +import { createExtensionRegistrationCallback } from './ui_extensions'; + +describe('UI Extension services', () => { + describe('When using createExtensionRegistrationCallback factory', () => { + let storage: UIExtensionsStorage; + let register: UIExtensionRegistrationCallback; + + beforeEach(() => { + storage = {}; + register = createExtensionRegistrationCallback(storage); + }); + + it('should return a function', () => { + expect(register).toBeInstanceOf(Function); + }); + + it('should store an extension points', () => { + const LazyCustomView = lazy(async () => { + return { default: ((() => {}) as unknown) as PackagePolicyEditExtensionComponent }; + }); + register({ + view: 'package-policy-edit', + package: 'endpoint', + component: LazyCustomView, + }); + + expect(storage.endpoint['package-policy-edit']).toEqual({ + view: 'package-policy-edit', + package: 'endpoint', + component: LazyCustomView, + }); + }); + + it('should throw if extension point has already registered', () => { + const LazyCustomView = lazy(async () => { + return { default: ((() => {}) as unknown) as PackagePolicyEditExtensionComponent }; + }); + const LazyCustomView2 = lazy(async () => { + return { default: ((() => {}) as unknown) as PackagePolicyEditExtensionComponent }; + }); + + register({ + view: 'package-policy-edit', + package: 'endpoint', + component: LazyCustomView, + }); + + expect(() => { + register({ + view: 'package-policy-edit', + package: 'endpoint', + component: LazyCustomView2, + }); + }).toThrow(); + + expect(storage.endpoint['package-policy-edit']).toEqual({ + view: 'package-policy-edit', + package: 'endpoint', + component: LazyCustomView, + }); + }); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/services/ui_extensions.ts b/x-pack/plugins/fleet/public/applications/fleet/services/ui_extensions.ts new file mode 100644 index 00000000000000..5af9122d4f12a3 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/services/ui_extensions.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 { UIExtensionRegistrationCallback, UIExtensionsStorage } from '../types'; + +/** Factory that returns a callback that can be used to register UI extensions */ +export const createExtensionRegistrationCallback = ( + storage: UIExtensionsStorage +): UIExtensionRegistrationCallback => { + return (extensionPoint) => { + const { package: packageName, view } = extensionPoint; + + if (!storage[packageName]) { + storage[packageName] = {}; + } + + if (storage[packageName]?.[view]) { + throw new Error(`Extension point has already been registered: [${packageName}][${view}]`); + } + + storage[packageName][view] = extensionPoint; + }; +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/types/index.ts b/x-pack/plugins/fleet/public/applications/fleet/types/index.ts index 1cf8077aeda404..78cb355318d405 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/types/index.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/types/index.ts @@ -119,3 +119,5 @@ export { } from '../../../../common'; export * from './intra_app_route_state'; + +export * from './ui_extensions'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/types/ui_extensions.ts b/x-pack/plugins/fleet/public/applications/fleet/types/ui_extensions.ts new file mode 100644 index 00000000000000..fbede8af95b664 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/types/ui_extensions.ts @@ -0,0 +1,96 @@ +/* + * 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 { ComponentType, LazyExoticComponent } from 'react'; +import { NewPackagePolicy, PackagePolicy } from './index'; + +/** Register a Fleet UI extension */ +export type UIExtensionRegistrationCallback = (extensionPoint: UIExtensionPoint) => void; + +/** Internal storage for registered UI Extension Points */ +export interface UIExtensionsStorage { + [key: string]: Partial>; +} + +/** + * UI Component Extension is used on the pages displaying the ability to edit an + * Integration Policy + */ +export type PackagePolicyEditExtensionComponent = ComponentType< + PackagePolicyEditExtensionComponentProps +>; + +export interface PackagePolicyEditExtensionComponentProps { + /** The current integration policy being edited */ + policy: PackagePolicy; + /** The new (updated) integration policy that will be saved */ + newPolicy: NewPackagePolicy; + /** + * A callback that should be executed anytime a change to the Integration Policy needs to + * be reported back to the Fleet Policy Edit page + */ + onChange: (opts: { + /** is current form state is valid */ + isValid: boolean; + /** The updated Integration Policy to be merged back and included in the API call */ + updatedPolicy: NewPackagePolicy; + }) => void; +} + +/** Extension point registration contract for Integration Policy Edit views */ +export interface PackagePolicyEditExtension { + package: string; + view: 'package-policy-edit'; + component: LazyExoticComponent; +} + +/** + * UI Component Extension is used on the pages displaying the ability to Create an + * Integration Policy + */ +export type PackagePolicyCreateExtensionComponent = ComponentType< + PackagePolicyCreateExtensionComponentProps +>; + +export interface PackagePolicyCreateExtensionComponentProps { + /** The integration policy being created */ + newPolicy: NewPackagePolicy; + /** + * A callback that should be executed anytime a change to the Integration Policy needs to + * be reported back to the Fleet Policy Edit page + */ + onChange: (opts: { + /** is current form state is valid */ + isValid: boolean; + /** The updated Integration Policy to be merged back and included in the API call */ + updatedPolicy: NewPackagePolicy; + }) => void; +} + +/** Extension point registration contract for Integration Policy Create views */ +export interface PackagePolicyCreateExtension { + package: string; + view: 'package-policy-create'; + component: LazyExoticComponent; +} + +/** + * UI Component Extension is used to display a Custom tab (and view) under a given Integration + */ +export type PackageCustomExtensionComponent = ComponentType; + +/** Extension point registration contract for Integration details Custom view */ +export interface PackageCustomExtension { + package: string; + view: 'package-detail-custom'; + component: LazyExoticComponent; +} + +/** Fleet UI Extension Point */ +export type UIExtensionPoint = + | PackagePolicyEditExtension + | PackageCustomExtension + | PackagePolicyCreateExtension; diff --git a/x-pack/plugins/fleet/public/index.ts b/x-pack/plugins/fleet/public/index.ts index f974a8c3d3cc81..1de001a6fc69e0 100644 --- a/x-pack/plugins/fleet/public/index.ts +++ b/x-pack/plugins/fleet/public/index.ts @@ -12,13 +12,8 @@ export const plugin = (initializerContext: PluginInitializerContext) => { return new IngestManagerPlugin(initializerContext); }; -export { - CustomConfigurePackagePolicyContent, - CustomConfigurePackagePolicyProps, - registerPackagePolicyComponent, -} from './applications/fleet/sections/agent_policy/create_package_policy_page/components/custom_package_policy'; - export type { NewPackagePolicy } from './applications/fleet/types'; export * from './applications/fleet/types/intra_app_route_state'; +export * from './applications/fleet/types/ui_extensions'; export { pagePathGetters } from './applications/fleet/constants'; diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts index 2e7cbb9cb86ab7..377ba770b5ca2b 100644 --- a/x-pack/plugins/fleet/public/plugin.ts +++ b/x-pack/plugins/fleet/public/plugin.ts @@ -30,7 +30,8 @@ import { TutorialDirectoryHeaderLink, TutorialModuleNotice, } from './applications/fleet/components/home_integration'; -import { registerPackagePolicyComponent } from './applications/fleet/sections/agent_policy/create_package_policy_page/components/custom_package_policy'; +import { createExtensionRegistrationCallback } from './applications/fleet/services/ui_extensions'; +import { UIExtensionRegistrationCallback, UIExtensionsStorage } from './applications/fleet/types'; export { IngestManagerConfigType } from '../common/types'; @@ -43,7 +44,7 @@ export interface IngestManagerSetup {} * Describes public IngestManager plugin contract returned at the `start` stage. */ export interface IngestManagerStart { - registerPackagePolicyComponent: typeof registerPackagePolicyComponent; + registerExtension: UIExtensionRegistrationCallback; isInitialized: () => Promise; } @@ -62,6 +63,7 @@ export class IngestManagerPlugin Plugin { private config: IngestManagerConfigType; private kibanaVersion: string; + private extensions: UIExtensionsStorage = {}; constructor(private readonly initializerContext: PluginInitializerContext) { this.config = this.initializerContext.config.get(); @@ -71,6 +73,7 @@ export class IngestManagerPlugin public setup(core: CoreSetup, deps: IngestManagerSetupDeps) { const config = this.config; const kibanaVersion = this.kibanaVersion; + const extensions = this.extensions; // Set up http client setHttpClient(core.http); @@ -92,7 +95,15 @@ export class IngestManagerPlugin IngestManagerStart ]; const { renderApp, teardownIngestManager } = await import('./applications/fleet/'); - const unmount = renderApp(coreStart, params, deps, startDeps, config, kibanaVersion); + const unmount = renderApp( + coreStart, + params, + deps, + startDeps, + config, + kibanaVersion, + extensions + ); return () => { unmount(); @@ -153,7 +164,8 @@ export class IngestManagerPlugin return successPromise; }, - registerPackagePolicyComponent, + + registerExtension: createExtensionRegistrationCallback(this.extensions), }; } diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts b/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts index ff3fe7517e64ab..3388fb53558451 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IngestManagerStart, registerPackagePolicyComponent } from '../../../../../fleet/public'; +import { IngestManagerStart } from '../../../../../fleet/public'; import { dataPluginMock, Start as DataPublicStartMock, @@ -58,7 +58,7 @@ export const depsStartMock: () => DepsStartMock = () => { data: dataMock, ingestManager: { isInitialized: () => Promise.resolve(true), - registerPackagePolicyComponent, + registerExtension: jest.fn(), }, }; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension.tsx new file mode 100644 index 00000000000000..69406a41fe0554 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo } from 'react'; +import { EuiCallOut, EuiSpacer, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { PackagePolicyCreateExtensionComponentProps } from '../../../../../../../fleet/public'; + +/** + * Exports Endpoint-specific package policy instructions + * for use in the Ingest app create / edit package policy + */ +export const EndpointPolicyCreateExtension = memo( + () => { + return ( + <> + + + +

+ +

+
+
+ + ); + } +); +EndpointPolicyCreateExtension.displayName = 'EndpointPolicyCreateExtension'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_package_policy.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx similarity index 81% rename from x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_package_policy.tsx rename to x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx index 0be5f119e5eff8..b667ea965af689 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_package_policy.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx @@ -20,9 +20,8 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { - CustomConfigurePackagePolicyContent, - CustomConfigurePackagePolicyProps, pagePathGetters, + PackagePolicyEditExtensionComponentProps, } from '../../../../../../../fleet/public'; import { getPolicyDetailPath, getTrustedAppsListPath } from '../../../../common/routing'; import { MANAGEMENT_APP_ID } from '../../../../common/constants'; @@ -37,42 +36,21 @@ import { useNavigateToAppEventHandler } from '../../../../../common/hooks/endpoi * Exports Endpoint-specific package policy instructions * for use in the Ingest app create / edit package policy */ -export const ConfigureEndpointPackagePolicy = memo( - ({ - from, - packagePolicyId, - packagePolicy: { policy_id: agentPolicyId }, - }: CustomConfigurePackagePolicyProps) => { +export const EndpointPolicyEditExtension = memo( + ({ policy }) => { return ( <> - + - {from === 'edit' ? ( - <> - - - ) : ( -

- -

- )} +
); } ); -ConfigureEndpointPackagePolicy.displayName = 'ConfigureEndpointPackagePolicy'; +EndpointPolicyEditExtension.displayName = 'EndpointPolicyEditExtension'; const EditFlowMessage = memo<{ agentPolicyId: string; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension.tsx new file mode 100644 index 00000000000000..b7a6fa36e4eb7c --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { lazy } from 'react'; +import { PackagePolicyCreateExtensionComponent } from '../../../../../../../fleet/public'; + +export const LazyEndpointPolicyCreateExtension = lazy( + async () => { + const { EndpointPolicyCreateExtension } = await import('./endpoint_policy_create_extension'); + return { + default: EndpointPolicyCreateExtension, + }; + } +); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension.tsx new file mode 100644 index 00000000000000..b417bc9ad5d9cf --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { lazy } from 'react'; +import { PackagePolicyEditExtensionComponent } from '../../../../../../../fleet/public'; + +export const LazyEndpointPolicyEditExtension = lazy( + async () => { + const { EndpointPolicyEditExtension } = await import('./endpoint_policy_edit_extension'); + return { + // FIXME: remove casting once old UI component registration is removed + default: (EndpointPolicyEditExtension as unknown) as PackagePolicyEditExtensionComponent, + }; + } +); diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 08c780d4a72032..5895880adb26a3 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import { BehaviorSubject } from 'rxjs'; import { pluck } from 'rxjs/operators'; - import { PluginSetup, PluginStart, @@ -44,8 +43,6 @@ import { DEFAULT_INDEX_KEY, } from '../common/constants'; -import { ConfigureEndpointPackagePolicy } from './management/pages/policy/view/ingest_manager_integration/configure_package_policy'; - import { SecurityPageName } from './app/types'; import { manageOldSiemRoutes } from './helpers'; import { @@ -63,6 +60,8 @@ import { } from '../common/search_strategy/index_fields'; import { SecurityAppStore } from './common/store/store'; import { getCaseConnectorUI } from './common/lib/connectors'; +import { LazyEndpointPolicyEditExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension'; +import { LazyEndpointPolicyCreateExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension'; export class Plugin implements IPlugin { private kibanaVersion: string; @@ -332,10 +331,19 @@ export class Plugin implements IPlugin Date: Thu, 12 Nov 2020 17:51:42 +0100 Subject: [PATCH 31/52] [ML] Update apidoc config with the Trained models endpoints (#83274) * [ML] fix apidoc annotations * [ML] add trained models * [ML] use full path to the apidoc-markdown package --- x-pack/plugins/ml/package.json | 2 +- x-pack/plugins/ml/server/routes/apidoc.json | 8 ++++- .../ml/server/routes/trained_models.ts | 30 +++++++++---------- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/ml/package.json b/x-pack/plugins/ml/package.json index a41e9e845063ff..1ec697568a8499 100644 --- a/x-pack/plugins/ml/package.json +++ b/x-pack/plugins/ml/package.json @@ -6,6 +6,6 @@ "license": "Elastic-License", "scripts": { "build:apiDocScripts": "cd server/routes/apidoc_scripts && ../../../../../../node_modules/.bin/tsc", - "apiDocs": "yarn build:apiDocScripts && cd ./server/routes/ && ../../../../../node_modules/.bin/apidoc --parse-workers apischema=./apidoc_scripts/target/schema_worker.js --parse-parsers apischema=./apidoc_scripts/target/schema_parser.js --parse-filters apiversion=./apidoc_scripts/target/version_filter.js -i . -o ../routes_doc && apidoc-markdown -p ../routes_doc -o ../routes_doc/ML_API.md -t ./apidoc_scripts/template.md" + "apiDocs": "yarn build:apiDocScripts && cd ./server/routes/ && ../../../../../node_modules/.bin/apidoc --parse-workers apischema=./apidoc_scripts/target/schema_worker.js --parse-parsers apischema=./apidoc_scripts/target/schema_parser.js --parse-filters apiversion=./apidoc_scripts/target/version_filter.js -i . -o ../routes_doc && ../../../../../node_modules/.bin/apidoc-markdown -p ../routes_doc -o ../routes_doc/ML_API.md -t ./apidoc_scripts/template.md" } } \ No newline at end of file diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index 780835e2a300bd..8d6dd692cc130c 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -148,6 +148,12 @@ "InitializeJobSavedObjects", "AssignJobsToSpaces", "RemoveJobsFromSpaces", - "JobsSpaces" + "JobsSpaces", + + "TrainedModels", + "GetTrainedModel", + "GetTrainedModelStats", + "GetTrainedModelPipelines", + "DeleteTrainedModel" ] } diff --git a/x-pack/plugins/ml/server/routes/trained_models.ts b/x-pack/plugins/ml/server/routes/trained_models.ts index 579f63e13328df..e9bd854864c2da 100644 --- a/x-pack/plugins/ml/server/routes/trained_models.ts +++ b/x-pack/plugins/ml/server/routes/trained_models.ts @@ -16,11 +16,11 @@ import { InferenceConfigResponse } from '../../common/types/trained_models'; export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization) { /** - * @apiGroup Inference + * @apiGroup TrainedModels * * @api {get} /api/ml/trained_models/:modelId Get info of a trained inference model - * @apiName GetInferenceModel - * @apiDescription Retrieves configuration information for a trained inference model. + * @apiName GetTrainedModel + * @apiDescription Retrieves configuration information for a trained model. */ router.get( { @@ -68,11 +68,11 @@ export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization) ); /** - * @apiGroup Inference + * @apiGroup TrainedModels * - * @api {get} /api/ml/trained_models/:modelId/_stats Get stats of a trained inference model - * @apiName GetInferenceModelStats - * @apiDescription Retrieves usage information for trained inference models. + * @api {get} /api/ml/trained_models/:modelId/_stats Get stats of a trained model + * @apiName GetTrainedModelStats + * @apiDescription Retrieves usage information for trained models. */ router.get( { @@ -100,11 +100,11 @@ export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization) ); /** - * @apiGroup Inference + * @apiGroup TrainedModels * - * @api {get} /api/ml/trained_models/:modelId/pipelines Get model pipelines - * @apiName GetModelPipelines - * @apiDescription Retrieves pipelines associated with a model + * @api {get} /api/ml/trained_models/:modelId/pipelines Get trained model pipelines + * @apiName GetTrainedModelPipelines + * @apiDescription Retrieves pipelines associated with a trained model */ router.get( { @@ -130,11 +130,11 @@ export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization) ); /** - * @apiGroup Inference + * @apiGroup TrainedModels * - * @api {delete} /api/ml/trained_models/:modelId Get stats of a trained inference model - * @apiName DeleteInferenceModel - * @apiDescription Deletes an existing trained inference model that is currently not referenced by an ingest pipeline. + * @api {delete} /api/ml/trained_models/:modelId Delete a trained model + * @apiName DeleteTrainedModel + * @apiDescription Deletes an existing trained model that is currently not referenced by an ingest pipeline. */ router.delete( { From 3151e7e5e4d2da37ebc69a047366863f04970be3 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 12 Nov 2020 17:31:44 +0000 Subject: [PATCH 32/52] enables actions scoped within the stack to register at Basic license (#82931) Enables actions scoped within the stack to register at Basic license --- .../server/builtin_action_types/es_index.ts | 3 +- .../server/builtin_action_types/server_log.ts | 3 +- .../lib/ensure_sufficient_license.test.ts | 68 +++++++++++++++++++ .../server/lib/ensure_sufficient_license.ts | 36 ++++++++++ x-pack/plugins/actions/server/plugin.ts | 11 +-- .../case/server/connectors/case/index.ts | 3 +- .../plugins/case/server/connectors/index.ts | 1 + x-pack/plugins/case/server/index.ts | 2 + 8 files changed, 115 insertions(+), 12 deletions(-) create mode 100644 x-pack/plugins/actions/server/lib/ensure_sufficient_license.test.ts create mode 100644 x-pack/plugins/actions/server/lib/ensure_sufficient_license.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts index 868c07b775c78f..6926c826f776e2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts @@ -39,10 +39,11 @@ const ParamsSchema = schema.object({ documents: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), }); +export const ES_INDEX_ACTION_TYPE_ID = '.index'; // action type definition export function getActionType({ logger }: { logger: Logger }): ESIndexActionType { return { - id: '.index', + id: ES_INDEX_ACTION_TYPE_ID, minimumLicenseRequired: 'basic', name: i18n.translate('xpack.actions.builtin.esIndexTitle', { defaultMessage: 'Index', diff --git a/x-pack/plugins/actions/server/builtin_action_types/server_log.ts b/x-pack/plugins/actions/server/builtin_action_types/server_log.ts index 490764fb16bfdb..c485de8628f142 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/server_log.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/server_log.ts @@ -38,10 +38,11 @@ const ParamsSchema = schema.object({ ), }); +export const SERVER_LOG_ACTION_TYPE_ID = '.server-log'; // action type definition export function getActionType({ logger }: { logger: Logger }): ServerLogActionType { return { - id: '.server-log', + id: SERVER_LOG_ACTION_TYPE_ID, minimumLicenseRequired: 'basic', name: i18n.translate('xpack.actions.builtin.serverLogTitle', { defaultMessage: 'Server log', diff --git a/x-pack/plugins/actions/server/lib/ensure_sufficient_license.test.ts b/x-pack/plugins/actions/server/lib/ensure_sufficient_license.test.ts new file mode 100644 index 00000000000000..845f8d2688f49a --- /dev/null +++ b/x-pack/plugins/actions/server/lib/ensure_sufficient_license.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ActionType } from '../types'; +import { ensureSufficientLicense } from './ensure_sufficient_license'; + +const sampleActionType: ActionType = { + id: 'test', + name: 'test', + minimumLicenseRequired: 'basic', + async executor({ actionId }) { + return { status: 'ok', actionId }; + }, +}; + +describe('ensureSufficientLicense()', () => { + it('throws for licenses below gold', () => { + expect(() => ensureSufficientLicense(sampleActionType)).toThrowErrorMatchingInlineSnapshot( + `"Third party action type \\"test\\" can only set minimumLicenseRequired to a gold license or higher"` + ); + }); + + it('allows licenses below gold for allowed connectors', () => { + expect(() => + ensureSufficientLicense({ ...sampleActionType, id: '.case', minimumLicenseRequired: 'basic' }) + ).not.toThrow(); + expect(() => + ensureSufficientLicense({ + ...sampleActionType, + id: '.server-log', + minimumLicenseRequired: 'basic', + }) + ).not.toThrow(); + expect(() => + ensureSufficientLicense({ + ...sampleActionType, + id: '.index', + minimumLicenseRequired: 'basic', + }) + ).not.toThrow(); + }); + + it('allows licenses at gold', () => { + expect(() => + ensureSufficientLicense({ ...sampleActionType, minimumLicenseRequired: 'gold' }) + ).not.toThrow(); + }); + + it('allows licenses above gold', () => { + expect(() => + ensureSufficientLicense({ ...sampleActionType, minimumLicenseRequired: 'platinum' }) + ).not.toThrow(); + }); + + it('throws when license type is invalid', async () => { + expect(() => + ensureSufficientLicense({ + ...sampleActionType, + // we're faking an invalid value, this requires stripping the typing + // eslint-disable-next-line @typescript-eslint/no-explicit-any + minimumLicenseRequired: 'foo' as any, + }) + ).toThrowErrorMatchingInlineSnapshot(`"\\"foo\\" is not a valid license type"`); + }); +}); diff --git a/x-pack/plugins/actions/server/lib/ensure_sufficient_license.ts b/x-pack/plugins/actions/server/lib/ensure_sufficient_license.ts new file mode 100644 index 00000000000000..0f309bb76b76c4 --- /dev/null +++ b/x-pack/plugins/actions/server/lib/ensure_sufficient_license.ts @@ -0,0 +1,36 @@ +/* + * 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 { ActionType } from '../types'; +import { LICENSE_TYPE } from '../../../licensing/common/types'; +import { SERVER_LOG_ACTION_TYPE_ID } from '../builtin_action_types/server_log'; +import { ES_INDEX_ACTION_TYPE_ID } from '../builtin_action_types/es_index'; +import { CASE_ACTION_TYPE_ID } from '../../../case/server'; +import { ActionTypeConfig, ActionTypeSecrets, ActionTypeParams } from '../types'; + +const ACTIONS_SCOPED_WITHIN_STACK = new Set([ + SERVER_LOG_ACTION_TYPE_ID, + ES_INDEX_ACTION_TYPE_ID, + CASE_ACTION_TYPE_ID, +]); + +export function ensureSufficientLicense< + Config extends ActionTypeConfig, + Secrets extends ActionTypeSecrets, + Params extends ActionTypeParams, + ExecutorResultData +>(actionType: ActionType) { + if (!(actionType.minimumLicenseRequired in LICENSE_TYPE)) { + throw new Error(`"${actionType.minimumLicenseRequired}" is not a valid license type`); + } + if ( + LICENSE_TYPE[actionType.minimumLicenseRequired] < LICENSE_TYPE.gold && + !ACTIONS_SCOPED_WITHIN_STACK.has(actionType.id) + ) { + throw new Error( + `Third party action type "${actionType.id}" can only set minimumLicenseRequired to a gold license or higher` + ); + } +} diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 599e7461ea3128..9db07f653872fe 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -27,7 +27,6 @@ import { } from '../../encrypted_saved_objects/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server'; -import { LICENSE_TYPE } from '../../licensing/common/types'; import { SpacesPluginSetup, SpacesServiceSetup } from '../../spaces/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { SecurityPluginSetup } from '../../security/server'; @@ -75,6 +74,7 @@ import { getAuthorizationModeBySource, AuthorizationMode, } from './authorization/get_authorization_mode_by_source'; +import { ensureSufficientLicense } from './lib/ensure_sufficient_license'; const EVENT_LOG_PROVIDER = 'actions'; export const EVENT_LOG_ACTIONS = { @@ -260,14 +260,7 @@ export class ActionsPlugin implements Plugin, Plugi >( actionType: ActionType ) => { - if (!(actionType.minimumLicenseRequired in LICENSE_TYPE)) { - throw new Error(`"${actionType.minimumLicenseRequired}" is not a valid license type`); - } - if (LICENSE_TYPE[actionType.minimumLicenseRequired] < LICENSE_TYPE.gold) { - throw new Error( - `Third party action type "${actionType.id}" can only set minimumLicenseRequired to a gold license or higher` - ); - } + ensureSufficientLicense(actionType); actionTypeRegistry.register(actionType); }, }; diff --git a/x-pack/plugins/case/server/connectors/case/index.ts b/x-pack/plugins/case/server/connectors/case/index.ts index f284f0ed9668cf..f2f8f659f3a2c0 100644 --- a/x-pack/plugins/case/server/connectors/case/index.ts +++ b/x-pack/plugins/case/server/connectors/case/index.ts @@ -23,6 +23,7 @@ import { GetActionTypeParams } from '..'; const supportedSubActions: string[] = ['create', 'update', 'addComment']; +export const CASE_ACTION_TYPE_ID = '.case'; // action type definition export function getActionType({ logger, @@ -31,7 +32,7 @@ export function getActionType({ userActionService, }: GetActionTypeParams): CaseActionType { return { - id: '.case', + id: CASE_ACTION_TYPE_ID, minimumLicenseRequired: 'gold', name: i18n.NAME, validate: { diff --git a/x-pack/plugins/case/server/connectors/index.ts b/x-pack/plugins/case/server/connectors/index.ts index 6a97a9e6e8a8a8..bee7b1e475457d 100644 --- a/x-pack/plugins/case/server/connectors/index.ts +++ b/x-pack/plugins/case/server/connectors/index.ts @@ -19,6 +19,7 @@ import { } from '../services'; import { getActionType as getCaseConnector } from './case'; +export { CASE_ACTION_TYPE_ID } from './case'; export interface GetActionTypeParams { logger: Logger; diff --git a/x-pack/plugins/case/server/index.ts b/x-pack/plugins/case/server/index.ts index f924810baa9126..d4f06c8a2304c8 100644 --- a/x-pack/plugins/case/server/index.ts +++ b/x-pack/plugins/case/server/index.ts @@ -8,6 +8,8 @@ import { PluginInitializerContext } from '../../../../src/core/server'; import { ConfigSchema } from './config'; import { CasePlugin } from './plugin'; +export { CASE_ACTION_TYPE_ID } from './connectors'; +export { CaseRequestContext } from './types'; export const config = { schema: ConfigSchema }; export const plugin = (initializerContext: PluginInitializerContext) => new CasePlugin(initializerContext); From 6519b83e4879152f0955cb9383e43db1425a0a9a Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 12 Nov 2020 12:40:08 -0500 Subject: [PATCH 33/52] [ML] Data frame analytics: Adds map view (#81666) * add analytics map endpoint and server model * add map action to job and models list * wip:fetch models for jobs. Use url generator * get models when extending node. deduplicate elements * add job type icons. disable map action if job not finished. * move shared const to common dir * persist map tab. handle indexPattern from visualizer * use url generator in models list * temporarily disable delete action in flyout * update legend style. make map horizontal * update dfa model to use spaces changes * format creation time * update from indexPattern to index.remove refresh button * handle index patterns with wildcard --- .../common/constants/data_frame_analytics.ts | 9 + .../ml/common/constants/ml_url_generator.ts | 1 + .../ml/common/types/ml_url_generator.ts | 2 +- .../plugins/ml/common/util/analytics_utils.ts | 15 +- .../data_frame_analytics/_index.scss | 1 + .../data_frame_analytics/common/analytics.ts | 14 +- .../components/action_map/index.ts | 8 + .../components/action_map/map_button.tsx | 51 ++ .../components/action_map/use_map_action.tsx | 44 ++ .../components/analytics_list/use_actions.tsx | 3 + .../analytics_navigation_bar.tsx | 26 +- .../models_management/models_list.tsx | 31 +- .../pages/analytics_management/page.tsx | 16 +- .../pages/job_map/components/_index.scss | 1 + .../pages/job_map/components/_legend.scss | 38 ++ .../pages/job_map/components/controls.tsx | 165 ++++++ .../pages/job_map/components/cytoscape.tsx | 113 ++++ .../job_map/components/cytoscape_options.tsx | 125 +++++ .../job_map/components/delete_button.tsx | 58 ++ .../icons/ml_classification_job.svg | 7 + .../icons/ml_outlier_detection_job.svg | 7 + .../components/icons/ml_regression_job.svg | 4 + .../pages/job_map/components/index.ts | 9 + .../pages/job_map/components/legend.tsx | 62 +++ .../job_map/components/use_ref_dimensions.ts | 21 + .../pages/job_map/index.ts | 7 + .../pages/job_map/job_map.tsx | 134 +++++ .../data_frame_analytics/analytics_map.tsx | 44 ++ .../routes/data_frame_analytics/index.ts | 1 + .../ml_api_service/data_frame_analytics.ts | 8 + .../data_frame_analytics_urls_generator.ts | 32 ++ .../ml_url_generator/ml_url_generator.ts | 5 + .../data_frame_analytics/analytics_manager.ts | 495 ++++++++++++++++++ .../models/data_frame_analytics/index.ts | 1 + .../models/data_frame_analytics/types.ts | 73 +++ .../ml/server/routes/data_frame_analytics.ts | 48 ++ .../routes/schemas/data_analytics_schema.ts | 5 + 37 files changed, 1657 insertions(+), 27 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/index.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/map_button.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_index.scss create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_legend.scss create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/delete_button.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/icons/ml_classification_job.svg create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/icons/ml_outlier_detection_job.svg create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/icons/ml_regression_job.svg create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/index.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/use_ref_dimensions.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/index.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx create mode 100644 x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_map.tsx create mode 100644 x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts create mode 100644 x-pack/plugins/ml/server/models/data_frame_analytics/types.ts diff --git a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts index 9a7af2496c03fe..5c8000566bb38a 100644 --- a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts @@ -10,3 +10,12 @@ export const ANALYSIS_CONFIG_TYPE = { CLASSIFICATION: 'classification', } as const; export const DEFAULT_RESULTS_FIELD = 'ml'; + +export const JOB_MAP_NODE_TYPES = { + ANALYTICS: 'analytics', + TRANSFORM: 'transform', + INDEX: 'index', + INFERENCE_MODEL: 'inferenceModel', +} as const; + +export type JobMapNodeTypes = typeof JOB_MAP_NODE_TYPES[keyof typeof JOB_MAP_NODE_TYPES]; diff --git a/x-pack/plugins/ml/common/constants/ml_url_generator.ts b/x-pack/plugins/ml/common/constants/ml_url_generator.ts index 541b8af6fc0fc7..a79e72a84c08e4 100644 --- a/x-pack/plugins/ml/common/constants/ml_url_generator.ts +++ b/x-pack/plugins/ml/common/constants/ml_url_generator.ts @@ -12,6 +12,7 @@ export const ML_PAGES = { SINGLE_METRIC_VIEWER: 'timeseriesexplorer', DATA_FRAME_ANALYTICS_JOBS_MANAGE: 'data_frame_analytics', DATA_FRAME_ANALYTICS_EXPLORATION: 'data_frame_analytics/exploration', + DATA_FRAME_ANALYTICS_MAP: 'data_frame_analytics/map', /** * Page: Data Visualizer */ diff --git a/x-pack/plugins/ml/common/types/ml_url_generator.ts b/x-pack/plugins/ml/common/types/ml_url_generator.ts index aa38fb2ec6fbbe..b188ac0a87571e 100644 --- a/x-pack/plugins/ml/common/types/ml_url_generator.ts +++ b/x-pack/plugins/ml/common/types/ml_url_generator.ts @@ -159,7 +159,7 @@ export interface DataFrameAnalyticsQueryState { } export type DataFrameAnalyticsUrlState = MLPageState< - typeof ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE, + typeof ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE | typeof ML_PAGES.DATA_FRAME_ANALYTICS_MAP, DataFrameAnalyticsQueryState | undefined >; diff --git a/x-pack/plugins/ml/common/util/analytics_utils.ts b/x-pack/plugins/ml/common/util/analytics_utils.ts index 94797efdfcfadf..5da9d270a44a75 100644 --- a/x-pack/plugins/ml/common/util/analytics_utils.ts +++ b/x-pack/plugins/ml/common/util/analytics_utils.ts @@ -10,7 +10,8 @@ import { OutlierAnalysis, RegressionAnalysis, } from '../types/data_frame_analytics'; -import { ANALYSIS_CONFIG_TYPE } from '../../common/constants/data_frame_analytics'; +import { ANALYSIS_CONFIG_TYPE } from '../constants/data_frame_analytics'; +import { DataFrameAnalysisConfigType } from '../types/data_frame_analytics'; export const isOutlierAnalysis = (arg: any): arg is OutlierAnalysis => { if (typeof arg !== 'object' || arg === null) return false; @@ -80,3 +81,15 @@ export const getPredictedFieldName = ( }`; return predictedField; }; + +export const getAnalysisType = ( + analysis: AnalysisConfig +): DataFrameAnalysisConfigType | 'unknown' => { + const keys = Object.keys(analysis || {}); + + if (keys.length === 1) { + return keys[0] as DataFrameAnalysisConfigType; + } + + return 'unknown'; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/_index.scss b/x-pack/plugins/ml/public/application/data_frame_analytics/_index.scss index 231d0f6a0d8c55..a043a691c9ef66 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/_index.scss +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/_index.scss @@ -1,4 +1,5 @@ @import 'pages/analytics_exploration/components/regression_exploration/index'; +@import 'pages/job_map/components/index'; @import 'pages/analytics_management/components/analytics_list/index'; @import 'pages/analytics_management/components/create_analytics_button/index'; @import 'pages/analytics_creation/components/index'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 6e42e3e2f51faf..a99270a3e3be69 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -27,6 +27,8 @@ import { getPredictedFieldName, } from '../../../../common/util/analytics_utils'; import { ANALYSIS_CONFIG_TYPE } from '../../../../common/constants/data_frame_analytics'; + +export { getAnalysisType } from '../../../../common/util/analytics_utils'; export type IndexPattern = string; export enum ANALYSIS_ADVANCED_FIELDS { @@ -159,18 +161,6 @@ interface LoadEvaluateResult { error: string | null; } -export const getAnalysisType = ( - analysis: AnalysisConfig -): DataFrameAnalysisConfigType | 'unknown' => { - const keys = Object.keys(analysis); - - if (keys.length === 1) { - return keys[0] as DataFrameAnalysisConfigType; - } - - return 'unknown'; -}; - export const getTrainingPercent = ( analysis: AnalysisConfig ): diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/index.ts new file mode 100644 index 00000000000000..4004f019c732bd --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { MapButton } from './map_button'; +export { useMapAction } from './use_map_action'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/map_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/map_button.tsx new file mode 100644 index 00000000000000..28016b421aff3f --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/map_button.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiToolTip } from '@elastic/eui'; + +import { + isRegressionAnalysis, + isOutlierAnalysis, + isClassificationAnalysis, +} from '../../../../common/analytics'; + +import { DataFrameAnalyticsListRow } from '../analytics_list/common'; + +export const mapActionButtonText = i18n.translate( + 'xpack.ml.dataframe.analyticsList.mapActionName', + { + defaultMessage: 'Map', + } +); +interface MapButtonProps { + item: DataFrameAnalyticsListRow; +} + +export const MapButton: FC = ({ item }) => { + const disabled = + !isRegressionAnalysis(item.config.analysis) && + !isOutlierAnalysis(item.config.analysis) && + !isClassificationAnalysis(item.config.analysis); + + if (disabled) { + const toolTipContent = i18n.translate( + 'xpack.ml.dataframe.analyticsList.mapActionDisabledTooltipContent', + { + defaultMessage: 'Unknown analysis type.', + } + ); + + return ( + + <>{mapActionButtonText} + + ); + } + + return <>{mapActionButtonText}; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx new file mode 100644 index 00000000000000..f77f71dcee4e74 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useMemo } from 'react'; +import { useMlUrlGenerator, useNavigateToPath } from '../../../../../contexts/kibana'; +import { DataFrameAnalyticsListAction, DataFrameAnalyticsListRow } from '../analytics_list/common'; +import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator'; +import { getViewLinkStatus } from '../action_view/get_view_link_status'; + +import { mapActionButtonText, MapButton } from './map_button'; + +export type MapAction = ReturnType; +export const useMapAction = () => { + const mlUrlGenerator = useMlUrlGenerator(); + const navigateToPath = useNavigateToPath(); + + const clickHandler = useCallback(async (item: DataFrameAnalyticsListRow) => { + const path = await mlUrlGenerator.createUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_MAP, + pageState: { jobId: item.id }, + }); + + await navigateToPath(path, false); + }, []); + + const action: DataFrameAnalyticsListAction = useMemo( + () => ({ + isPrimary: true, + name: (item: DataFrameAnalyticsListRow) => , + enabled: (item: DataFrameAnalyticsListRow) => !getViewLinkStatus(item).disabled, + description: mapActionButtonText, + icon: 'graphApp', + type: 'icon', + onClick: clickHandler, + 'data-test-subj': 'mlAnalyticsJobMapButton', + }), + [clickHandler] + ); + + return { action }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx index 20ae48a12ecf9c..74b367cc7ab13a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx @@ -16,6 +16,7 @@ import { isEditActionFlyoutVisible, useEditAction, EditActionFlyout } from '../a import { useStartAction, StartActionModal } from '../action_start'; import { useStopAction, StopActionModal } from '../action_stop'; import { useViewAction } from '../action_view'; +import { useMapAction } from '../action_map'; import { DataFrameAnalyticsListRow } from './common'; @@ -30,6 +31,7 @@ export const useActions = ( const canStartStopDataFrameAnalytics: boolean = checkPermission('canStartStopDataFrameAnalytics'); const viewAction = useViewAction(); + const mapAction = useMapAction(); const cloneAction = useCloneAction(canCreateDataFrameAnalytics); const deleteAction = useDeleteAction(canDeleteDataFrameAnalytics); const editAction = useEditAction(canStartStopDataFrameAnalytics); @@ -40,6 +42,7 @@ export const useActions = ( const actions: EuiTableActionsColumnType['actions'] = [ viewAction.action, + mapAction.action, ]; // isManagementTable will be the same for the lifecycle of the component diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx index bd59749517052a..eaeae6cc645202 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx @@ -15,11 +15,14 @@ interface Tab { path: string; } -export const AnalyticsNavigationBar: FC<{ selectedTabId?: string }> = ({ selectedTabId }) => { +export const AnalyticsNavigationBar: FC<{ selectedTabId?: string; jobId?: string }> = ({ + jobId, + selectedTabId, +}) => { const navigateToPath = useNavigateToPath(); - const tabs = useMemo( - () => [ + const tabs = useMemo(() => { + const navTabs = [ { id: 'data_frame_analytics', name: i18n.translate('xpack.ml.dataframe.jobsTabLabel', { @@ -34,12 +37,21 @@ export const AnalyticsNavigationBar: FC<{ selectedTabId?: string }> = ({ selecte }), path: '/data_frame_analytics/models', }, - ], - [] - ); + ]; + if (jobId !== undefined) { + navTabs.push({ + id: 'map', + name: i18n.translate('xpack.ml.dataframe.mapTabLabel', { + defaultMessage: 'Map', + }), + path: '/data_frame_analytics/map', + }); + } + return navTabs; + }, [jobId !== undefined]); const onTabClick = useCallback(async (tab: Tab) => { - await navigateToPath(tab.path); + await navigateToPath(tab.path, true); }, []); return ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx index 713cbbc814c324..a87f11df937d34 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx @@ -28,8 +28,14 @@ import { StatsBar, ModelsBarStats } from '../../../../../components/stats_bar'; import { useTrainedModelsApiService } from '../../../../../services/ml_api_service/trained_models'; import { ModelsTableToConfigMapping } from './index'; import { DeleteModelsModal } from './delete_models_modal'; -import { useMlKibana, useMlUrlGenerator, useNotifications } from '../../../../../contexts/kibana'; +import { + useMlKibana, + useMlUrlGenerator, + useNavigateToPath, + useNotifications, +} from '../../../../../contexts/kibana'; import { ExpandedRow } from './expanded_row'; + import { TrainedModelConfigResponse, ModelPipelines, @@ -80,6 +86,9 @@ export const ModelsList: FC = () => { {} ); + const mlUrlGenerator = useMlUrlGenerator(); + const navigateToPath = useNavigateToPath(); + const updateFilteredItems = (queryClauses: any) => { if (queryClauses.length) { const filtered = filterAnalyticsModels(items, queryClauses); @@ -298,6 +307,26 @@ export const ModelsList: FC = () => { }, isPrimary: true, }, + { + name: i18n.translate('xpack.ml.inference.modelsList.analyticsMapActionLabel', { + defaultMessage: 'Analytics map', + }), + description: i18n.translate('xpack.ml.inference.modelsList.analyticsMapActionLabel', { + defaultMessage: 'Analytics map', + }), + icon: 'graphApp', + type: 'icon', + isPrimary: true, + available: (item) => item.metadata?.analytics_config?.id, + onClick: async (item) => { + const path = await mlUrlGenerator.createUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_MAP, + pageState: { jobId: item.metadata?.analytics_config.id }, + }); + + await navigateToPath(path, false); + }, + }, { name: i18n.translate('xpack.ml.trainedModels.modelsList.deleteModelActionLabel', { defaultMessage: 'Delete model', diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx index 7ffd477039e78f..44085384f7536a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx @@ -22,6 +22,7 @@ import { } from '@elastic/eui'; import { useLocation } from 'react-router-dom'; +import { useUrlState } from '../../../util/url_state'; import { NavigationMenu } from '../../../components/navigation_menu'; import { DatePickerWrapper } from '../../../components/navigation_menu/date_picker_wrapper'; import { DataFrameAnalyticsList } from './components/analytics_list'; @@ -31,14 +32,17 @@ import { NodeAvailableWarning } from '../../../components/node_available_warning import { UpgradeWarning } from '../../../components/upgrade'; import { AnalyticsNavigationBar } from './components/analytics_navigation_bar'; import { ModelsList } from './components/models_management'; +import { JobMap } from '../job_map'; export const Page: FC = () => { const [blockRefresh, setBlockRefresh] = useState(false); + const [globalState] = useUrlState('_g'); useRefreshInterval(setBlockRefresh); const location = useLocation(); const selectedTabId = useMemo(() => location.pathname.split('/').pop(), [location]); + const mapJobId = globalState?.ml?.jobId; return ( @@ -73,9 +77,11 @@ export const Page: FC = () => { - - - + {selectedTabId !== 'map' && ( + + + + )} @@ -87,8 +93,8 @@ export const Page: FC = () => { - - + + {selectedTabId === 'map' && mapJobId && } {selectedTabId === 'data_frame_analytics' && ( )} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_index.scss b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_index.scss new file mode 100644 index 00000000000000..2bcc91f34d3823 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_index.scss @@ -0,0 +1 @@ +@import 'legend'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_legend.scss b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_legend.scss new file mode 100644 index 00000000000000..d54b5214f7448e --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/_legend.scss @@ -0,0 +1,38 @@ +.mlJobMapLegend__container { + background-color: '#FFFFFF'; +} + +.mlJobMapLegend__indexPattern { + height: $euiSizeM; + width: $euiSizeM; + background-color: '#FFFFFF'; + border: 1px solid $euiColorVis2; + transform: rotate(45deg); + display: 'inline-block'; +} + +.mlJobMapLegend__transform { + height: $euiSizeM; + width: $euiSizeM; + background-color: '#FFFFFF'; + border: 1px solid $euiColorVis1; + display: 'inline-block'; +} + +.mlJobMapLegend__analytics { + height: $euiSizeM; + width: $euiSizeM; + background-color: '#FFFFFF'; + border: 1px solid $euiColorVis0; + border-radius: 50%; + display: 'inline-block'; +} + +.mlJobMapLegend__inferenceModel { + height: $euiSizeM; + width: $euiSizeM; + background-color: '#FFFFFF'; + border: 1px solid $euiColorMediumShade; + border-radius: 50%; + display: 'inline-block'; +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx new file mode 100644 index 00000000000000..ed25ea6cbf02c0 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useEffect, useState, useContext, useCallback } from 'react'; +import cytoscape from 'cytoscape'; +import { FormattedMessage } from '@kbn/i18n/react'; +import moment from 'moment-timezone'; +import { + EuiButtonEmpty, + EuiCodeBlock, + EuiDescriptionList, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiPortal, + EuiTitle, +} from '@elastic/eui'; +import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list'; +import { CytoscapeContext } from './cytoscape'; +import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/util/date_utils'; +import { JOB_MAP_NODE_TYPES } from '../../../../../../common/constants/data_frame_analytics'; +// import { DeleteButton } from './delete_button'; + +interface Props { + analyticsId: string; + details: any; + getNodeData: any; +} + +function getListItems(details: object): EuiDescriptionListProps['listItems'] { + return Object.entries(details).map(([key, value]) => { + let description; + if (key === 'create_time') { + description = formatHumanReadableDateTimeSeconds(moment(value).unix() * 1000); + } else { + description = + typeof value === 'object' ? ( + + {JSON.stringify(value, null, 2)} + + ) : ( + value + ); + } + + return { + title: key, + description, + }; + }); +} + +export const Controls: FC = ({ analyticsId, details, getNodeData }) => { + const [showFlyout, setShowFlyout] = useState(false); + const [selectedNode, setSelectedNode] = useState(); + + const cy = useContext(CytoscapeContext); + const deselect = useCallback(() => { + if (cy) { + cy.elements().unselect(); + } + setShowFlyout(false); + setSelectedNode(undefined); + }, [cy, setSelectedNode]); + + const nodeId = selectedNode?.data('id'); + const nodeLabel = selectedNode?.data('label'); + const nodeType = selectedNode?.data('type'); + + // Set up Cytoscape event handlers + useEffect(() => { + const selectHandler: cytoscape.EventHandler = (event) => { + setSelectedNode(event.target); + setShowFlyout(true); + }; + + if (cy) { + cy.on('select', 'node', selectHandler); + cy.on('unselect', 'node', deselect); + } + + return () => { + if (cy) { + cy.removeListener('select', 'node', selectHandler); + cy.removeListener('unselect', 'node', deselect); + } + }; + }, [cy, deselect]); + + if (showFlyout === false) { + return null; + } + + const nodeDataButton = + analyticsId !== nodeLabel && nodeType === JOB_MAP_NODE_TYPES.ANALYTICS ? ( + { + getNodeData(nodeLabel); + setShowFlyout(false); + }} + iconType="branch" + > + + + ) : null; + + return ( + + setShowFlyout(false)} + data-test-subj="mlAnalyticsJobMapFlyout" + > + + + + +

+ +

+
+
+
+
+ + + + + + + + + + {nodeDataButton} + {/* + + */} + + +
+
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx new file mode 100644 index 00000000000000..a901e2be06dc07 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx @@ -0,0 +1,113 @@ +/* + * 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, { + CSSProperties, + useState, + useRef, + useEffect, + ReactNode, + createContext, + useCallback, +} from 'react'; +import cytoscape from 'cytoscape'; +// @ts-ignore no declaration file +import dagre from 'cytoscape-dagre'; +import { cytoscapeOptions } from './cytoscape_options'; + +cytoscape.use(dagre); + +export const CytoscapeContext = createContext(undefined); + +interface CytoscapeProps { + children?: ReactNode; + elements: cytoscape.ElementDefinition[]; + height: number; + style?: CSSProperties; + width: number; +} + +function useCytoscape(options: cytoscape.CytoscapeOptions) { + const [cy, setCy] = useState(); + const ref = useRef(null); + + useEffect(() => { + if (!cy) { + setCy(cytoscape({ ...options, container: ref.current })); + } + }, [options, cy]); + + // Destroy the cytoscape instance on unmount + useEffect(() => { + return () => { + if (cy) { + cy.destroy(); + } + }; + }, [cy]); + + return [ref, cy] as [React.MutableRefObject, cytoscape.Core | undefined]; +} + +function getLayoutOptions(width: number, height: number) { + return { + name: 'dagre', + rankDir: 'LR', + fit: true, + padding: 30, + spacingFactor: 0.85, + boundingBox: { x1: 0, y1: 0, w: width, h: height }, + }; +} + +export function Cytoscape({ children, elements, height, style, width }: CytoscapeProps) { + const [ref, cy] = useCytoscape({ + ...cytoscapeOptions, + elements, + }); + + // Add the height to the div style. The height is a separate prop because it + // is required and can trigger rendering when changed. + const divStyle = { ...style, height }; + + const dataHandler = useCallback( + (event) => { + if (cy && height > 0) { + cy.layout(getLayoutOptions(width, height)).run(); + } + }, + [cy, height, width] + ); + + // Set up cytoscape event handlers + useEffect(() => { + if (cy) { + cy.on('data', dataHandler); + } + + return () => { + if (cy) { + cy.removeListener('data', undefined, dataHandler as cytoscape.EventHandler); + } + }; + }, [cy, elements, height, width]); + + // Trigger a custom "data" event when data changes + useEffect(() => { + if (cy) { + cy.add(elements); + cy.trigger('data'); + } + }, [cy, elements]); + + return ( + +
+ {children} +
+
+ ); +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx new file mode 100644 index 00000000000000..85d10aa897415d --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape_options.tsx @@ -0,0 +1,125 @@ +/* + * 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 cytoscape from 'cytoscape'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { + ANALYSIS_CONFIG_TYPE, + JOB_MAP_NODE_TYPES, +} from '../../../../../../common/constants/data_frame_analytics'; +import classificationJobIcon from './icons/ml_classification_job.svg'; +import outlierDetectionJobIcon from './icons/ml_outlier_detection_job.svg'; +import regressionJobIcon from './icons/ml_regression_job.svg'; + +const lineColor = '#C5CCD7'; + +const MAP_SHAPES = { + ELLIPSE: 'ellipse', + RECTANGLE: 'rectangle', + DIAMOND: 'diamond', +} as const; +type MapShapes = typeof MAP_SHAPES[keyof typeof MAP_SHAPES]; + +function shapeForNode(el: cytoscape.NodeSingular): MapShapes { + const type = el.data('type'); + switch (type) { + case JOB_MAP_NODE_TYPES.ANALYTICS: + return MAP_SHAPES.ELLIPSE; + case JOB_MAP_NODE_TYPES.TRANSFORM: + return MAP_SHAPES.RECTANGLE; + case JOB_MAP_NODE_TYPES.INDEX: + return MAP_SHAPES.DIAMOND; + default: + return MAP_SHAPES.ELLIPSE; + } +} + +function iconForNode(el: cytoscape.NodeSingular) { + const type = el.data('analysisType'); + + switch (type) { + case ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION: + return outlierDetectionJobIcon; + case ANALYSIS_CONFIG_TYPE.CLASSIFICATION: + return classificationJobIcon; + case ANALYSIS_CONFIG_TYPE.REGRESSION: + return regressionJobIcon; + default: + return undefined; + } +} + +function borderColorForNode(el: cytoscape.NodeSingular) { + if (el.selected()) { + return theme.euiColorPrimary; + } + + const type = el.data('type'); + + switch (type) { + case JOB_MAP_NODE_TYPES.ANALYTICS: + return theme.euiColorSecondary; + case JOB_MAP_NODE_TYPES.TRANSFORM: + return theme.euiColorVis1; + case JOB_MAP_NODE_TYPES.INDEX: + return theme.euiColorVis2; + default: + return theme.euiColorMediumShade; + } +} + +export const cytoscapeOptions: cytoscape.CytoscapeOptions = { + autoungrabify: true, + boxSelectionEnabled: false, + maxZoom: 3, + minZoom: 0.2, + style: [ + { + selector: 'node', + style: { + 'background-color': theme.euiColorGhost, + 'background-height': '60%', + 'background-width': '60%', + 'border-color': (el: cytoscape.NodeSingular) => borderColorForNode(el), + 'border-style': 'solid', + // @ts-ignore + 'background-image': (el: cytoscape.NodeSingular) => iconForNode(el), + 'border-width': (el: cytoscape.NodeSingular) => (el.selected() ? 2 : 1), + // @ts-ignore + color: theme.textColors.default, + 'font-family': 'Inter UI, Segoe UI, Helvetica, Arial, sans-serif', + 'font-size': theme.euiFontSizeXS, + 'min-zoomed-font-size': parseInt(theme.euiSizeL, 10), + label: 'data(label)', + shape: (el: cytoscape.NodeSingular) => shapeForNode(el), + 'text-background-color': theme.euiColorLightestShade, + 'text-background-opacity': 0, + 'text-background-padding': theme.paddingSizes.xs, + 'text-background-shape': 'roundrectangle', + 'text-margin-y': parseInt(theme.paddingSizes.s, 10), + 'text-max-width': '200px', + 'text-valign': 'bottom', + 'text-wrap': 'wrap', + }, + }, + { + selector: 'edge', + style: { + 'curve-style': 'taxi', + // @ts-ignore + 'taxi-direction': 'rightward', + 'line-color': lineColor, + 'overlay-opacity': 0, + 'target-arrow-color': lineColor, + 'target-arrow-shape': 'triangle', + // @ts-ignore + 'target-distance-from-node': theme.paddingSizes.xs, + width: 1, + 'source-arrow-shape': 'none', + }, + }, + ], +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/delete_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/delete_button.tsx new file mode 100644 index 00000000000000..523d24a3a39812 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/delete_button.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ml } from '../../../../services/ml_api_service'; +import { getToastNotifications } from '../../../../util/dependency_cache'; +import { + JOB_MAP_NODE_TYPES, + JobMapNodeTypes, +} from '../../../../../../common/constants/data_frame_analytics'; + +interface Props { + id: string; + type: JobMapNodeTypes; +} + +export const DeleteButton: FC = ({ id, type }) => { + const toastNotifications = getToastNotifications(); + + const onDelete = async () => { + try { + // if (isDataFrameAnalyticsFailed(d.stats.state)) { + // await ml.dataFrameAnalytics.stopDataFrameAnalytics(d.config.id, true); + // } + await ml.dataFrameAnalytics.deleteDataFrameAnalytics(id); + toastNotifications.addSuccess( + i18n.translate('xpack.ml.dataframe.analyticsMap.flyout.deleteAnalyticsSuccessMessage', { + defaultMessage: 'Request to delete data frame analytics {id} acknowledged.', + values: { id }, + }) + ); + } catch (e) { + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataframe.analyticsMap.flyout.deleteAnalyticsErrorMessage', { + defaultMessage: 'An error occurred deleting the data frame analytics {id}: {error}', + values: { id, error: JSON.stringify(e) }, + }) + ); + } + }; + + if (type !== JOB_MAP_NODE_TYPES.ANALYTICS) { + return null; + } + + return ( + + {i18n.translate('xpack.ml.dataframe.analyticsMap.flyout.deleteJobButton', { + defaultMessage: 'Delete job', + })} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/icons/ml_classification_job.svg b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/icons/ml_classification_job.svg new file mode 100644 index 00000000000000..5659de836b1db2 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/icons/ml_classification_job.svg @@ -0,0 +1,7 @@ + + + + diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/icons/ml_outlier_detection_job.svg b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/icons/ml_outlier_detection_job.svg new file mode 100644 index 00000000000000..293a0fff7b1ec8 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/icons/ml_outlier_detection_job.svg @@ -0,0 +1,7 @@ + + + + diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/icons/ml_regression_job.svg b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/icons/ml_regression_job.svg new file mode 100644 index 00000000000000..9bdc5a79522f78 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/icons/ml_regression_job.svg @@ -0,0 +1,4 @@ + + + + diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/index.ts new file mode 100644 index 00000000000000..7f99bb88ec0c8f --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { Cytoscape, CytoscapeContext } from './cytoscape'; +export { Controls } from './controls'; +export { JobMapLegend } from './legend'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx new file mode 100644 index 00000000000000..c29b6aca804d70 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/legend.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { JOB_MAP_NODE_TYPES } from '../../../../../../common/constants/data_frame_analytics'; + +export const JobMapLegend: FC = () => ( + + + + + + + + + {JOB_MAP_NODE_TYPES.INDEX} + + + + + + + + + + + + {JOB_MAP_NODE_TYPES.TRANSFORM} + + + + + + + + + + + + {JOB_MAP_NODE_TYPES.ANALYTICS} + + + + + + + + + + + + {'inference model'} + + + + + +); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/use_ref_dimensions.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/use_ref_dimensions.ts new file mode 100644 index 00000000000000..fc478e27ccac39 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/use_ref_dimensions.ts @@ -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 { useRef } from 'react'; +import useWindowSize from 'react-use/lib/useWindowSize'; + +export function useRefDimensions() { + const ref = useRef(null); + const windowHeight = useWindowSize().height; + + if (!ref.current) { + return { ref, width: 0, height: 0 }; + } + + const { top, width } = ref.current.getBoundingClientRect(); + const height = windowHeight - top; + + return { ref, width, height }; +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/index.ts new file mode 100644 index 00000000000000..59d94bb22980c5 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/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 { JobMap } from './job_map'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx new file mode 100644 index 00000000000000..53d47937409d81 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useEffect, useState } from 'react'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; +import cytoscape from 'cytoscape'; +import { uniqWith, isEqual } from 'lodash'; + +import { Cytoscape, Controls, JobMapLegend } from './components'; +import { ml } from '../../../services/ml_api_service'; +import { useMlKibana } from '../../../contexts/kibana'; +import { useRefDimensions } from './components/use_ref_dimensions'; + +const cytoscapeDivStyle = { + background: `linear-gradient( + 90deg, + ${theme.euiPageBackgroundColor} + calc(${theme.euiSizeL} - calc(${theme.euiSizeXS} / 2)), + transparent 1% +) +center, +linear-gradient( + ${theme.euiPageBackgroundColor} + calc(${theme.euiSizeL} - calc(${theme.euiSizeXS} / 2)), + transparent 1% +) +center, +${theme.euiColorLightShade}`, + backgroundSize: `${theme.euiSizeL} ${theme.euiSizeL}`, + margin: `-${theme.gutterTypes.gutterLarge}`, + marginTop: 0, +}; + +export const JobMapTitle: React.FC<{ analyticsId: string }> = ({ analyticsId }) => ( + + + {i18n.translate('xpack.ml.dataframe.analyticsMap.analyticsIdTitle', { + defaultMessage: 'Map for analytics ID {analyticsId}', + values: { analyticsId }, + })} + + +); + +interface Props { + analyticsId: string; +} + +export const JobMap: FC = ({ analyticsId }) => { + const [elements, setElements] = useState([]); + const [nodeDetails, setNodeDetails] = useState({}); + const [error, setError] = useState(undefined); + + const { + services: { notifications }, + } = useMlKibana(); + + const getData = async (id?: string) => { + const treatAsRoot = id !== undefined; + const idToUse = treatAsRoot ? id : analyticsId; + // Pass in treatAsRoot flag - endpoint will take job destIndex to grab jobs created from it + // TODO: update analyticsMap return type here + const analyticsMap: any = await ml.dataFrameAnalytics.getDataFrameAnalyticsMap( + idToUse, + treatAsRoot + ); + + const { elements: nodeElements, details, error: fetchError } = analyticsMap; + + if (fetchError !== null) { + setError(fetchError); + } + + if (nodeElements && nodeElements.length === 0) { + notifications.toasts.add( + i18n.translate('xpack.ml.dataframe.analyticsMap.emptyResponseMessage', { + defaultMessage: 'No related analytics jobs found for {id}.', + values: { id: idToUse }, + }) + ); + } + + if (nodeElements && nodeElements.length > 0) { + if (id === undefined) { + setElements(nodeElements); + setNodeDetails(details); + } else { + const uniqueElements = uniqWith([...nodeElements, ...elements], isEqual); + setElements(uniqueElements); + setNodeDetails({ ...details, ...nodeDetails }); + } + } + }; + + useEffect(() => { + getData(); + }, [analyticsId]); + + if (error !== undefined) { + notifications.toasts.addDanger( + i18n.translate('xpack.ml.dataframe.analyticsMap.fetchDataErrorMessage', { + defaultMessage: 'Unable to fetch some data. An error occurred: {error}', + values: { error: JSON.stringify(error) }, + }) + ); + setError(undefined); + } + + const { ref, width, height } = useRefDimensions(); + + return ( + <> + +
+ + + + + + + + + + + +
+ + ); +}; diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_map.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_map.tsx new file mode 100644 index 00000000000000..18002648cfaa6c --- /dev/null +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_map.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../../contexts/kibana'; + +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; +import { basicResolvers } from '../../resolvers'; +import { Page } from '../../../data_frame_analytics/pages/analytics_management'; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; + +export const analyticsMapRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ + path: '/data_frame_analytics/map', + render: (props, deps) => , + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath, basePath), + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.analyticsMapLabel', { + defaultMessage: 'Analytics Map', + }), + href: '', + }, + ], +}); + +const PageWrapper: FC = ({ deps }) => { + const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/index.ts b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/index.ts index c75a8240d28fb2..eedcaaf41292b5 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/index.ts +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/index.ts @@ -8,3 +8,4 @@ export * from './analytics_jobs_list'; export * from './analytics_job_exploration'; export * from './analytics_job_creation'; export * from './models_list'; +export * from './analytics_map'; diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts index 7de39d91047ef1..21556a4702b4e5 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts @@ -83,6 +83,14 @@ export const dataFrameAnalytics = { body, }); }, + getDataFrameAnalyticsMap(analyticsId?: string, treatAsRoot?: boolean) { + const analyticsIdString = analyticsId !== undefined ? `/${analyticsId}` : ''; + return http({ + path: `${basePath()}/data_frame/analytics/map${analyticsIdString}`, + method: 'GET', + query: { treatAsRoot }, + }); + }, evaluateDataFrameAnalytics(evaluateConfig: any) { const body = JSON.stringify(evaluateConfig); return http({ diff --git a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts index 2408290e76773d..6c58a9d28bcc20 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts @@ -82,3 +82,35 @@ export function createDataFrameAnalyticsExplorationUrl( return url; } + +/** + * Creates URL to the DataFrameAnalytics Map page + */ +export function createDataFrameAnalyticsMapUrl( + appBasePath: string, + mlUrlGeneratorState: DataFrameAnalyticsExplorationUrlState['pageState'] +): string { + let url = `${appBasePath}/${ML_PAGES.DATA_FRAME_ANALYTICS_MAP}`; + + if (mlUrlGeneratorState) { + const { jobId, analysisType, defaultIsTraining, globalState } = mlUrlGeneratorState; + + const queryState: DataFrameAnalyticsExplorationQueryState = { + ml: { + jobId, + analysisType, + defaultIsTraining, + }, + ...globalState, + }; + + url = setStateToKbnUrl( + '_g', + queryState, + { useHash: false, storeInHashQuery: false }, + url + ); + } + + return url; +} diff --git a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts index 351e366d1f1d89..a3f1ed6f78e8d5 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts @@ -23,6 +23,7 @@ import { import { createDataFrameAnalyticsJobManagementUrl, createDataFrameAnalyticsExplorationUrl, + createDataFrameAnalyticsMapUrl, } from './data_frame_analytics_urls_generator'; import { createGenericMlUrl } from './common'; import { createEditCalendarUrl, createEditFilterUrl } from './settings_urls_generator'; @@ -68,6 +69,10 @@ export class MlUrlGenerator implements UrlGeneratorsDefinition { + if (elem.data.label === analyticsId && elem.data.type === JOB_MAP_NODE_TYPES.ANALYTICS) { + isDuplicate = true; + } + }); + return isDuplicate; + } + // @ts-ignore // TODO: is this needed? + private async getAnalyticsModelData(modelId: string) { + const resp = await this._mlClient.getTrainedModels({ + model_id: modelId, + }); + const modelData = resp?.body?.trained_model_configs[0]; + return modelData; + } + + private async getAnalyticsModels() { + const resp = await this._mlClient.getTrainedModels(); + const models = resp?.body?.trained_model_configs; + return models; + } + + private async getAnalyticsJobData(analyticsId: string) { + const resp = await this._mlClient.getDataFrameAnalytics({ + id: analyticsId, + }); + const jobData = resp?.body?.data_frame_analytics[0]; + return jobData; + } + + private async getIndexData(index: string) { + const indexData = await this._client.indices.get({ + index, + }); + + return indexData?.body; + } + + private async getTransformData(transformId: string) { + const transform = await this._client.transform.getTransform({ + transform_id: transformId, + }); + const transformData = transform?.body?.transforms[0]; + return transformData; + } + + private findJobModel(analyticsId: string): any { + return this.inferenceModels.find( + (model: any) => model.metadata?.analytics_config?.id === analyticsId + ); + } + + private async getNextLink({ + id, + type, + }: { + id: string; + type: JobMapNodeTypes; + }): Promise { + try { + if (type === JOB_MAP_NODE_TYPES.INDEX) { + // fetch index data + const indexData = await this.getIndexData(id); + let isWildcardIndexPattern = false; + + if (id.includes('*')) { + isWildcardIndexPattern = true; + } + const meta = indexData[id]?.mappings?._meta; + return { isWildcardIndexPattern, isIndexPattern: true, indexData, meta }; + } else if (type.includes(JOB_MAP_NODE_TYPES.ANALYTICS)) { + // fetch job associated with this index + const jobData = await this.getAnalyticsJobData(id); + return { jobData, isJob: true }; + } else if (type === JOB_MAP_NODE_TYPES.TRANSFORM) { + // fetch transform so we can get original index pattern + const transformData = await this.getTransformData(id); + return { transformData, isTransform: true }; + } + } catch (error) { + throw Boom.badData(error.message ? error.message : error); + } + } + + private getAnalyticsModelElements( + analyticsId: string + ): { + modelElement?: AnalyticsMapNodeElement; + modelDetails?: any; + edgeElement?: AnalyticsMapEdgeElement; + } { + // Get inference model for analytics job and create model node + const analyticsModel = this.findJobModel(analyticsId); + let modelElement; + let edgeElement; + + if (analyticsModel !== undefined) { + const modelId = `${analyticsModel.model_id}-${JOB_MAP_NODE_TYPES.INFERENCE_MODEL}`; + modelElement = { + data: { + id: modelId, + label: analyticsModel.model_id, + type: JOB_MAP_NODE_TYPES.INFERENCE_MODEL, + }, + }; + // Create edge for job and corresponding model + edgeElement = { + data: { + id: `${analyticsId}-${JOB_MAP_NODE_TYPES.ANALYTICS}~${modelId}`, + source: `${analyticsId}-${JOB_MAP_NODE_TYPES.ANALYTICS}`, + target: modelId, + }, + }; + } + + return { modelElement, modelDetails: analyticsModel, edgeElement }; + } + + private getIndexPatternElements(indexData: Record, previousNodeId: string) { + const result: any = { elements: [], details: {} }; + + Object.keys(indexData).forEach((indexId) => { + // Create index node + const nodeId = `${indexId}-${JOB_MAP_NODE_TYPES.INDEX}`; + result.elements.push({ + data: { id: nodeId, label: indexId, type: JOB_MAP_NODE_TYPES.INDEX }, + }); + result.details[nodeId] = indexData[indexId]; + + // create edge node + result.elements.push({ + data: { + id: `${previousNodeId}~${nodeId}`, + source: nodeId, + target: previousNodeId, + }, + }); + }); + + return result; + } + + /** + * Works backward from jobId to return related jobs from source indices + * @param jobId + */ + async getAnalyticsMap(analyticsId: string): Promise { + const result: any = { elements: [], details: {}, error: null }; + const modelElements: MapElements[] = []; + const indexPatternElements: MapElements[] = []; + + try { + await this.setInferenceModels(); + // Create first node for incoming analyticsId + let data = await this.getAnalyticsJobData(analyticsId); + let nextLinkId = data?.source?.index[0]; + let nextType: JobMapNodeTypes = JOB_MAP_NODE_TYPES.INDEX; + let complete = false; + let link: NextLinkReturnType; + let count = 0; + let rootTransform; + let rootIndexPattern; + + let previousNodeId = `${data.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; + + result.elements.push({ + data: { + id: previousNodeId, + label: data.id, + type: JOB_MAP_NODE_TYPES.ANALYTICS, + analysisType: getAnalysisType(data?.analysis), + }, + }); + result.details[previousNodeId] = data; + + let { modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements(analyticsId); + if (isAnalyticsMapNodeElement(modelElement)) { + modelElements.push(modelElement); + result.details[modelElement.data.id] = modelDetails; + } + if (isAnalyticsMapEdgeElement(edgeElement)) { + modelElements.push(edgeElement); + } + // Add a safeguard against infinite loops. + while (complete === false) { + count++; + if (count >= 100) { + break; + } + + try { + link = await this.getNextLink({ + id: nextLinkId, + type: nextType, + }); + } catch (error) { + result.error = error.message || 'Something went wrong'; + break; + } + // If it's index pattern, check meta data to see what to fetch next + if (isIndexPatternLinkReturnType(link) && link.isIndexPattern === true) { + if (link.isWildcardIndexPattern === true) { + // Create index nodes for each of the indices included in the index pattern then break + const { details, elements } = this.getIndexPatternElements( + link.indexData, + previousNodeId + ); + + indexPatternElements.push(...elements); + result.details = { ...result.details, ...details }; + complete = true; + } else { + const nodeId = `${nextLinkId}-${JOB_MAP_NODE_TYPES.INDEX}`; + result.elements.unshift({ + data: { id: nodeId, label: nextLinkId, type: JOB_MAP_NODE_TYPES.INDEX }, + }); + result.details[nodeId] = link.indexData; + } + + // Check meta data + if ( + link.isWildcardIndexPattern === false && + (link.meta === undefined || link.meta?.created_by === INDEX_META_DATA_CREATED_BY) + ) { + rootIndexPattern = nextLinkId; + complete = true; + break; + } + + if (link.meta?.created_by === 'data-frame-analytics') { + nextLinkId = link.meta.analytics; + nextType = JOB_MAP_NODE_TYPES.ANALYTICS; + } + + if (link.meta?.created_by === JOB_MAP_NODE_TYPES.TRANSFORM) { + nextLinkId = link.meta._transform?.transform; + nextType = JOB_MAP_NODE_TYPES.TRANSFORM; + } + } else if (isJobDataLinkReturnType(link) && link.isJob === true) { + data = link.jobData; + const nodeId = `${data.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; + previousNodeId = nodeId; + + result.elements.unshift({ + data: { + id: nodeId, + label: data.id, + type: JOB_MAP_NODE_TYPES.ANALYTICS, + analysisType: getAnalysisType(data?.analysis), + }, + }); + result.details[nodeId] = data; + nextLinkId = data?.source?.index[0]; + nextType = JOB_MAP_NODE_TYPES.INDEX; + + // Get inference model for analytics job and create model node + ({ modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements(data.id)); + if (isAnalyticsMapNodeElement(modelElement)) { + modelElements.push(modelElement); + result.details[modelElement.data.id] = modelDetails; + } + if (isAnalyticsMapEdgeElement(edgeElement)) { + modelElements.push(edgeElement); + } + } else if (isTransformLinkReturnType(link) && link.isTransform === true) { + data = link.transformData; + + const nodeId = `${data.id}-${JOB_MAP_NODE_TYPES.TRANSFORM}`; + previousNodeId = nodeId; + rootTransform = data.dest.index; + + result.elements.unshift({ + data: { id: nodeId, label: data.id, type: JOB_MAP_NODE_TYPES.TRANSFORM }, + }); + result.details[nodeId] = data; + nextLinkId = data?.source?.index[0]; + nextType = JOB_MAP_NODE_TYPES.INDEX; + } + } // end while + + // create edge elements + const elemLength = result.elements.length - 1; + for (let i = 0; i < elemLength; i++) { + const currentElem = result.elements[i]; + const nextElem = result.elements[i + 1]; + if ( + currentElem !== undefined && + nextElem !== undefined && + currentElem?.data?.id.includes('*') === false && + nextElem?.data?.id.includes('*') === false + ) { + result.elements.push({ + data: { + id: `${currentElem.data.id}~${nextElem.data.id}`, + source: currentElem.data.id, + target: nextElem.data.id, + }, + }); + } + } + + // fetch all jobs associated with root transform if defined, otherwise check root index + if (rootTransform !== undefined || rootIndexPattern !== undefined) { + const analyticsJobs = await this._mlClient.getDataFrameAnalytics(); + const jobs = analyticsJobs?.body?.data_frame_analytics || []; + const comparator = rootTransform !== undefined ? rootTransform : rootIndexPattern; + + for (let i = 0; i < jobs.length; i++) { + if ( + jobs[i]?.source?.index[0] === comparator && + this.isDuplicateElement(jobs[i].id, result.elements) === false + ) { + const nodeId = `${jobs[i].id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; + result.elements.push({ + data: { + id: nodeId, + label: jobs[i].id, + type: JOB_MAP_NODE_TYPES.ANALYTICS, + analysisType: getAnalysisType(jobs[i]?.analysis), + }, + }); + result.details[nodeId] = jobs[i]; + const source = `${comparator}-${JOB_MAP_NODE_TYPES.INDEX}`; + result.elements.push({ + data: { + id: `${source}~${nodeId}`, + source, + target: nodeId, + }, + }); + // Get inference model for analytics job and create model node + ({ modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements( + jobs[i].id + )); + if (isAnalyticsMapNodeElement(modelElement)) { + modelElements.push(modelElement); + result.details[modelElement.data.id] = modelDetails; + } + if (isAnalyticsMapEdgeElement(edgeElement)) { + modelElements.push(edgeElement); + } + } + } + } + // Include model and index pattern nodes in result elements now that all other nodes have been created + result.elements.push(...modelElements, ...indexPatternElements); + + return result; + } catch (error) { + result.error = error.message || 'An error occurred fetching map'; + return result; + } + } + + async extendAnalyticsMapForAnalyticsJob(analyticsId: string): Promise { + const result: any = { elements: [], details: {}, error: null }; + + try { + await this.setInferenceModels(); + + const jobData = await this.getAnalyticsJobData(analyticsId); + const currentJobNodeId = `${jobData.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; + const destIndex = Array.isArray(jobData?.dest?.index) + ? jobData?.dest?.index[0] + : jobData?.dest?.index; + const destIndexNodeId = `${destIndex}-${JOB_MAP_NODE_TYPES.INDEX}`; + const analyticsJobs = await this._mlClient.getDataFrameAnalytics(); + const jobs = analyticsJobs?.body?.data_frame_analytics || []; + + // Fetch inference model for incoming job id and add node and edge + const { modelElement, modelDetails, edgeElement } = this.getAnalyticsModelElements( + analyticsId + ); + if (isAnalyticsMapNodeElement(modelElement)) { + result.elements.push(modelElement); + result.details[modelElement.data.id] = modelDetails; + } + if (isAnalyticsMapEdgeElement(edgeElement)) { + result.elements.push(edgeElement); + } + + // If destIndex node has not been created, create it + const destIndexDetails = await this.getIndexData(destIndex); + result.elements.push({ + data: { + id: destIndexNodeId, + label: destIndex, + type: JOB_MAP_NODE_TYPES.INDEX, + }, + }); + result.details[destIndexNodeId] = destIndexDetails; + + // Connect incoming job to destIndex + result.elements.push({ + data: { + id: `${currentJobNodeId}~${destIndexNodeId}`, + source: currentJobNodeId, + target: destIndexNodeId, + }, + }); + + for (let i = 0; i < jobs.length; i++) { + if ( + jobs[i]?.source?.index[0] === destIndex && + this.isDuplicateElement(jobs[i].id, result.elements) === false + ) { + // Create node for associated job + const nodeId = `${jobs[i].id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`; + result.elements.push({ + data: { + id: nodeId, + label: jobs[i].id, + type: JOB_MAP_NODE_TYPES.ANALYTICS, + analysisType: getAnalysisType(jobs[i]?.analysis), + }, + }); + result.details[nodeId] = jobs[i]; + + result.elements.push({ + data: { + id: `${destIndexNodeId}~${nodeId}`, + source: destIndexNodeId, + target: nodeId, + }, + }); + } + } + } catch (error) { + result.error = error.message || 'An error occurred fetching map'; + return result; + } + + return result; + } +} diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/index.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/index.ts index fc18436ff5216a..95c5d2848ae7a2 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/index.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/index.ts @@ -6,3 +6,4 @@ export { analyticsAuditMessagesProvider } from './analytics_audit_messages'; export { modelsProvider } from './models_provider'; +export { AnalyticsManager } from './analytics_manager'; diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/types.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/types.ts new file mode 100644 index 00000000000000..5d6cec8cdfa61a --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/types.ts @@ -0,0 +1,73 @@ +/* + * 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 IndexPatternLinkReturnType { + isWildcardIndexPattern: boolean; + isIndexPattern: boolean; + indexData: any; + meta: any; +} +export interface JobDataLinkReturnType { + isJob: boolean; + jobData: any; +} +export interface TransformLinkReturnType { + isTransform: boolean; + transformData: any; +} +export type NextLinkReturnType = + | IndexPatternLinkReturnType + | JobDataLinkReturnType + | TransformLinkReturnType + | undefined; +export type MapElements = AnalyticsMapNodeElement | AnalyticsMapEdgeElement; +export interface AnalyticsMapReturnType { + elements: MapElements[]; + details: object; // transform, job, or index details + error: null | any; +} +export interface AnalyticsMapNodeElement { + data: { + id: string; + label: string; + type: string; + analysisType?: string; + }; +} +export interface AnalyticsMapEdgeElement { + data: { + id: string; + source: string; + target: string; + }; +} +export const isAnalyticsMapNodeElement = (arg: any): arg is AnalyticsMapNodeElement => { + if (typeof arg !== 'object' || arg === null) return false; + const keys = Object.keys(arg); + return keys.length > 0 && keys.includes('data') && arg.data.label !== undefined; +}; +export const isAnalyticsMapEdgeElement = (arg: any): arg is AnalyticsMapEdgeElement => { + if (typeof arg !== 'object' || arg === null) return false; + const keys = Object.keys(arg); + return keys.length > 0 && keys.includes('data') && arg.data.target !== undefined; +}; +export const isIndexPatternLinkReturnType = (arg: any): arg is IndexPatternLinkReturnType => { + if (typeof arg !== 'object' || arg === null) return false; + const keys = Object.keys(arg); + return keys.length > 0 && keys.includes('isIndexPattern'); +}; + +export const isJobDataLinkReturnType = (arg: any): arg is JobDataLinkReturnType => { + if (typeof arg !== 'object' || arg === null) return false; + const keys = Object.keys(arg); + return keys.length > 0 && keys.includes('isJob'); +}; + +export const isTransformLinkReturnType = (arg: any): arg is TransformLinkReturnType => { + if (typeof arg !== 'object' || arg === null) return false; + const keys = Object.keys(arg); + return keys.length > 0 && keys.includes('isTransform'); +}; diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index 4364202a5c9af1..8e00ae70684031 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -14,14 +14,17 @@ import { dataAnalyticsEvaluateSchema, dataAnalyticsExplainSchema, analyticsIdSchema, + analyticsMapQuerySchema, stopsDataFrameAnalyticsJobQuerySchema, deleteDataFrameAnalyticsJobSchema, jobsExistSchema, } from './schemas/data_analytics_schema'; import { IndexPatternHandler } from '../models/data_frame_analytics/index_patterns'; +import { AnalyticsManager } from '../models/data_frame_analytics/analytics_manager'; import { DeleteDataFrameAnalyticsWithIndexStatus } from '../../common/types/data_frame_analytics'; import { getAuthorizationHeader } from '../lib/request_authorization'; import { DataFrameAnalyticsConfig } from '../../common/types/data_frame_analytics'; +import type { MlClient } from '../lib/ml_client'; function getIndexPatternId(context: RequestHandlerContext, patternName: string) { const iph = new IndexPatternHandler(context.core.savedObjects.client); @@ -33,6 +36,16 @@ function deleteDestIndexPatternById(context: RequestHandlerContext, indexPattern return iph.deleteIndexPatternById(indexPatternId); } +function getAnalyticsMap(mlClient: MlClient, client: IScopedClusterClient, analyticsId: string) { + const analytics = new AnalyticsManager(mlClient, client.asInternalUser); + return analytics.getAnalyticsMap(analyticsId); +} + +function getExtendedMap(mlClient: MlClient, client: IScopedClusterClient, analyticsId: string) { + const analytics = new AnalyticsManager(mlClient, client.asInternalUser); + return analytics.extendAnalyticsMapForAnalyticsJob(analyticsId); +} + /** * Routes for the data frame analytics */ @@ -598,4 +611,39 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense, routeGuard }: Rout } }) ); + + /** + * @apiGroup DataFrameAnalytics + * + * @api {get} /api/ml/data_frame/analytics/map/:analyticsId Get objects leading up to analytics job + * @apiName GetDataFrameAnalyticsIdMap + * @apiDescription Returns map of objects leading up to analytics job. + * + * @apiParam {String} analyticsId Analytics ID. + */ + router.get( + { + path: '/api/ml/data_frame/analytics/map/{analyticsId}', + validate: { + params: analyticsIdSchema, + query: analyticsMapQuerySchema, + }, + }, + routeGuard.fullLicenseAPIGuard(async ({ mlClient, client, request, response }) => { + try { + const { analyticsId } = request.params; + const treatAsRoot = request.query?.treatAsRoot; + const caller = + treatAsRoot === 'true' || treatAsRoot === true ? getExtendedMap : getAnalyticsMap; + + const results = await caller(mlClient, client, analyticsId); + + return response.ok({ + body: results, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); } diff --git a/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts index 846f79fbe0d8a6..d8226b70eb2c3f 100644 --- a/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts @@ -38,6 +38,7 @@ export const dataAnalyticsEvaluateSchema = schema.object({ schema.object({ regression: schema.maybe(schema.any()), classification: schema.maybe(schema.any()), + outlier_detection: schema.maybe(schema.any()), }) ), }); @@ -86,3 +87,7 @@ export const jobsExistSchema = schema.object({ analyticsIds: schema.arrayOf(schema.string()), allSpaces: schema.maybe(schema.boolean()), }); + +export const analyticsMapQuerySchema = schema.maybe( + schema.object({ treatAsRoot: schema.maybe(schema.any()) }) +); From 0e565bfd52f95da665f0508d0e768a00c53de39f Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Thu, 12 Nov 2020 09:43:45 -0800 Subject: [PATCH 34/52] [DOCS] Updates Discover docs (#82773) * [DOCS] Updates Discover docs * Update docs/user/discover.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/discover.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/discover.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/discover.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/discover.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/discover.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/discover.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/discover.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/discover.asciidoc Co-authored-by: Kaarina Tungseth * [DOCS] Incorporates review comments * [DOCS] More changes based on edits * [DOCS] Edits per lastest review * [DOCS] Added redirects Co-authored-by: Kaarina Tungseth --- docs/discover/context.asciidoc | 66 ----- docs/discover/document-data.asciidoc | 55 ---- docs/discover/field-filter.asciidoc | 155 ------------ docs/discover/images/Discover-Start.png | Bin 665101 -> 505443 bytes docs/discover/images/add-icon.png | Bin 0 -> 830 bytes .../images/discover-index-pattern.png | Bin 0 -> 14846 bytes .../images/document-table-expanded.png | Bin 0 -> 135694 bytes docs/discover/images/document-table.png | Bin 0 -> 202732 bytes docs/discover/images/filter-field.png | Bin 29276 -> 61584 bytes .../images/visualize-from-discover.png | Bin 0 -> 65498 bytes docs/discover/viewing-field-stats.asciidoc | 14 -- docs/redirects.asciidoc | 21 ++ docs/user/discover.asciidoc | 234 +++++++++++++----- 13 files changed, 189 insertions(+), 356 deletions(-) delete mode 100644 docs/discover/context.asciidoc delete mode 100644 docs/discover/document-data.asciidoc delete mode 100644 docs/discover/field-filter.asciidoc mode change 100644 => 100755 docs/discover/images/Discover-Start.png create mode 100644 docs/discover/images/add-icon.png create mode 100644 docs/discover/images/discover-index-pattern.png create mode 100755 docs/discover/images/document-table-expanded.png create mode 100644 docs/discover/images/document-table.png create mode 100644 docs/discover/images/visualize-from-discover.png delete mode 100644 docs/discover/viewing-field-stats.asciidoc diff --git a/docs/discover/context.asciidoc b/docs/discover/context.asciidoc deleted file mode 100644 index e26c91bfef0755..00000000000000 --- a/docs/discover/context.asciidoc +++ /dev/null @@ -1,66 +0,0 @@ -[[document-context]] -== View a document in context - -Once you've narrowed your search to a specific event, -you might want to inspect the documents that occurred -immediately before and after the event. With the Context view, -you can do just that for index patterns that contain time-based events. - -To open the Context view, click the expand icon (<) in the document table, and then click -*View surrounding documents.* - -The documents are sorted -by the time field specified in the index pattern and displayed using the -same set of columns as the *Discover* view from which the context was opened. -The anchor document is highlighted in blue. - - -[role="screenshot"] -image::images/Discover-ContextView.png[Image showing context view feature, with anchor documents highlighted in blue] - -[float] -[[filter-context]] -=== Filter the context - -The -filters you applied in *Discover* are carried over to the Context view. Pinned filters remain active, while normal -filters are copied in a disabled state. You can re-enable these filters to -refine your context view. - -If the Context view contains a large number of documents not related to the event under -investigation, you can use filters to restrict the documents to -display. - -[float] -[[change-context-size]] -=== Change the number of surrounding documents - -By default, the five newest and oldest -documents are listed. To increase the number of documents that surround the anchor document, -click *Load*. Five documents are added with each click. - -[float] -[[configure-context-ContextView]] -=== Configure the context view - -To configure the Context view, use these settings in <>. - -[horizontal] -`context:defaultSize`:: The number of documents to display by default. -`context:step`:: The default number of documents to load with each button click. -`context:tieBreakerFields`:: The field to use for tiebreaking in case of equal time field values. -The default is the -`_doc` field. -+ -You can enter a comma-separated list of field -names, which is checked in sequence for suitability when a context is -displayed. The first suitable field is used as the tiebreaking -field. A field is suitable if the field exists and is sortable in the index -pattern the context is based on. -+ -Although not required, it is recommended to only -use fields that have {ref}/doc-values.html[doc values] enabled to achieve -good performance and avoid unnecessary {ref}/modules-fielddata.html[field -data] usage. Common examples for suitable fields include log line numbers, -monotonically increasing counters and high-precision timestamps. diff --git a/docs/discover/document-data.asciidoc b/docs/discover/document-data.asciidoc deleted file mode 100644 index dd245e4b4558f2..00000000000000 --- a/docs/discover/document-data.asciidoc +++ /dev/null @@ -1,55 +0,0 @@ -[[document-data]] -== View document data - -When you submit a search query in *Discover*, the most recent documents that match the query -are listed in the documents table. -By default, the table includes columns for -the time field and the document `_source`, which shows all fields and values in the document. - -[float] -[[sorting]] -=== Modify the document table - -Use the following commands to -tailor the documents table to suit your needs. - -[horizontal] -Add a field column:: -Hover over the list of *Available fields* and then click *add* next to each field you want to include as a column in the table. -The first field you add replaces the `_source` column. -Change sort order:: By default, columns are sorted by the values in the field. -If a time field is configured for the current index pattern, -the documents are sorted in reverse chronological order. -+ -To change the sort order, hover over the column -and click image:images/sort-icon.png[]. -The first click sorts by ascending order, the second click sorts by descending order, and the third -click removes the field from the sorted fields. - -Move a field column:: Hover over the column header and click the (<<) or (>>) icons. -Remove a field column :: Hover over the list of *Specified fields* -and then click *remove*. -Or, use the (x) control in the column header. - -[float] -=== Drill down into field-level details -To view the document data in either table or JSON format, click the expand icon (>). -The expanded view provides these options for viewing your document: - -* View the events that surround your document. -For example, you might want to see the 10 documents that occurred -immediately before and after your event. - -* View the document data as a separate page. You can bookmark and -share the link for direct access to a particular document. - -[role="screenshot"] -image::images/Expanded-Document.png[Image showing expanded view, with JSON and table viewing options] - - -[float] -=== Configure the number of documents to show - -By default, the documents table includes the 500 most recent documents that -match the query. To change this number, set the `discover:sampleSize` property in <>. diff --git a/docs/discover/field-filter.asciidoc b/docs/discover/field-filter.asciidoc deleted file mode 100644 index 0c521b401e4b88..00000000000000 --- a/docs/discover/field-filter.asciidoc +++ /dev/null @@ -1,155 +0,0 @@ -[[field-filter]] -== Filter by field - -*Discover* offers -various types of filters, so you can restrict your documents to the exact data you want. -For example, you might look at the results for a -particular period of time. Or, you might include—or exclude— -all HTTP redirects that come from a specific IP and port. - -[float] -=== Add a filter - -A quick way to add a filter is from the fields list. - -. Click the field to filter on. -+ -You'll see the number of documents that contain -the field, the top 5 values for the field, and the percentage of documents -that contain each value. -+ -[role="screenshot"] -image::images/filter-field.png[Picture showing top 5 values for each field, and correspnding percentage of documents that contain each value] - -. Use the image:images/PositiveFilter.jpg[Positive Filter] icon to -show only documents that contain that value, -or image:images/NegativeFilter.jpg[Negative Filter] to exclude all documents with that value. -+ -If there is no data to display, you might need to set a <>. -You can choose a time from the quick filter or choose your -own using absolute or relative times. - -. Try also these filtering options: -+ -* To limit the field -list to a particular data type, click *Filter by type*. -You can also filter for whether that type is -aggregatable or searchable. -+ -* To filter for whether a field is present, expand the document in -the document table, hover over the field, and click the *Filter for field present* icon. - -[float] -=== Filter by condition - -You can filter using advanced criteria, -such as if a value is equal to or in between certain values. - -. Click *Add Filter*. - -. Select a field. - -. Select an operation for your filter: -+ -[horizontal] -`is`:: The value for the field matches the given value. -`is not`:: The value for the field does not match the given value. -`is one of`:: The field matches one of the specified values. -`is not one of`:: The value for the field does not match any of the specified values. -`is between`:: The value for the field is in the given range. -`is not between`:: The value for the field is not in the given range. -`exists`:: Any value is present for the field. -`does not exist`:: No value is present for the field. -. Choose values for your filter. -+ -Values from your indices may be suggested -as selections if you are filtering against an aggregatable field. - -. (Optional) Specify a label for the filter. - -. Click *Save* to apply the filter to your search. -+ -NOTE: If you are experiencing long-running queries as a result of the value suggestions, you can -turn off the suggestions by setting `filterEditor:suggestValues` to `false` -in <>. - -[float] -[[filter-pinning]] -=== Edit, disable, and delete filters - -To modify a filter, click its tag, and then select one of the following actions. - -*Pin across all apps*:: -Persist the filter -when you switch contexts in Kibana. For example, you can pin a filter -in *Discover* and it remains in place when you switch to *Visualize*. -A filter is based on a particular index field—if the indices being -searched do not contain the field in a pinned filter, it has no effect. - -*Edit filter*:: -Edit the -filter definition and label. - -*Exclude results*:: -Switch from a positive -filter to a negative filter, and vice versa. - -*Temporarily disable*:: -Disable the filter without -removing it. Click again to reenable the filter. - -*Delete*:: -Delete the filter. - -To apply an action to all filters, -click the *Actions* icon, and then select the action. - - - -[float] -[[filter-edit]] -=== Modify the filter query - -You can directly modify -the query that filters your search results. This enables you -to create more complex filters using multiple fields. - -. Click the filter tag, and then select *Edit > Edit Query DSL*. - -. Edit the query for the filter. -+ -//// -image::images/edit_filter_query_json.png[] -+ -//// -For example, if you are using the sample log data, you can use the -{ref}/query-dsl-bool-query.html[bool query] to create a filter -that displays the hits that originated from Canada or China that resulted in a 404 error: -+ -========== -[source,json] -{ - "bool": { - "should": [ - { - "term": { - "geoip.country_name.raw": "Canada" - } - }, - { - "term": { - "geoip.country_name.raw": "China" - } - } - ], - "must": [ - { - "term": { - "response": "404" - } - } - ] - } -} -========== diff --git a/docs/discover/images/Discover-Start.png b/docs/discover/images/Discover-Start.png old mode 100644 new mode 100755 index 12ec2f9889bbd43c3b989e333cfc343f2e5c6b2f..437684fdbcd799800f4c8f61c671a63d59f62f46 GIT binary patch literal 505443 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

U1tO$Rg6!v*92kVO$v|?es zvEWCg;;hhl}!6cCF`&7R*bMvfI6&Z~#t?$z2S8oy^!C zMFW#Q+aoqOwhw6JTEk-gGlmt6*r!hJt;W zh#~r+7i1_FAtoOsr{5L-+eBLq-xR0YYY_WXdRsUYlNx6({DQo4@?sw?`6J=2t88cg0e3V5v50O_W6e_lZrCQAm0}?y9%LzwsqAueC8Ti^#57 zLd^DZ3@XGVle3(>8h#A5kPG=2?ni`T&B`hFOVkEn9r@b3ZM~-yX4jComKBNM{6YyS zCasuUJ?ls-A5BDDW&Co*e;gTRr^Zq)-Hva6Rk!r=G$doHC}(85c@^jFl(0C-kfyGKLyP@-<^w#uNvneerz zav~o8w2`uocL!EQLIT9{aw5IM3gvVOl@6bpW@iDoyBM}!b<%_1+uS9R^B0V&lnL`S zO0JyDMRAV4YGe>i7{~Ji`&HbG{_^T9d`dP10$d!u2UgcC3XJFzwftv6_Qgx5b0H~i zQjF_@*=1p;7(P2l`7*h8GWXX25atHU9%CZ9wl!<1bkr5OG5J57DbFFF4OyKR#LS5d z*zJSov`6}Tbd*Y`pGh%doT_DEgsY`EDCHufp{_*WKpW|2*f*!uY#b*d)f`^!7J!)eP8Zp-?4fZOk6 zxw5tFE$W7S#cFbXn&$y_7v3xTMTbWMf1EI(<#?7}*a zqsp|wojN&pEqXeKHwF$lpI*;HegV^dMRdM}4@C3?6}LTUxB*v?06R8tQ^mRQ!_&)@ z#-;0TB5Q|CmFd=NeiN|`k9z5m=P{i7xlF8TBMqRPoyLG-zR<6cOG{^q!|X?(%ZJR> z?ego#I)3E-sB+P8p#SxQ?RixCMdt1jf=M(__Ql6FH_Lf}_3@WXC)26VQ4uhBHRzeg z%_~)#FPXNePJ3|khvJ-7jIiaoXX%L(n!E>rph|X0g4$S;;@O=Z{E4J+TxV;TE<(7o zcsQWcROUAD5X>+7svn9%m&$m2AFHdJp)abZR|3RaJZpO5*!8n{9Vn1#S=WW-yeRxW zLGJqGq#e^Jcj`Uvhb9bt=>$AWCF}c2ZyoD5yYr{p zPq74edS>i$`ifQ|#Ft;40J_Kx4l~L!m{N4U9MloWhf=Zk6^}}k!Kaq7W5H2rfd_4C zCxV8hL+w%aBf{-MycA`mZ)*CgZBcK~AK)d;zPgPcgQvd60?F|@dmRhrOYNkn#Dm%@ z%Xz{{%EfVYU_upNVE*TqMwBo4#@iBj{fEvtYVQ9&919UuMa$E`0!rmW0AiicRVMI1 zw~B|+7akV9hW_Q=BJ$LrOQ|-%T2E#0oIbpy4v;<2|9an^YfSvh3A1~ae$(8vN=v|I zJ^pUsW6BAGGdQrll+(0Y4(d`jqR`j3Y-froW62|>>zxuP9*>qu@367(_XCwe-680* z>CJ(zPB0%-#^R>oG?6b#`F=1ns(;cSIj}P-j~*{?b!1M1$gOSF^kd^`sVC{fjnxQU zjn5Xhx0AQ<;dSN)>xI|}HS~FLU3@qm6b)+Ikb>1ai=;L@&)2jL&&c&uV;&o=OA>zS z0tauPma(qlW?R$u)||g!ZR!9dY(nKa?BiuIlZc+?Ycfx6q3!Ef<+5_;R7k80*DnB2 z@5JGdhxg{+Ht(|>#5=-*C#5)bKGq}4e4=d6O`AdwP|b9ePvt?5k zo7U5-gG`bUusCi}zF>sSUVHWULD9eUg3zeJDWAzL@y6~16(Fw{A_$Dt%qm!=%(CCA zlff4e=efC6>?tjFsdxutp)=))K&4+*28Wcl=hCO`#&OufT+iQl z0r#w`eyw|Y(=L?fKxR(nomJkxJzvi}z0%KQ8l99*5^-`)b}0pT$=M-L&DNjqELu%%VMjgc1v6; z24h@2)XhD`sE76uOIULL+9JpmqP>tabzV||W0@g&Y)YPU5l0DO4iUH{0Lx)kB->{- zKsP)FwRAC;K~qHTw3M_(!s;9sCYbotMXOOqw=ZyFyxv8LtQ{9jv>C6f;86vXK7A?N zq-GWmonPm7=H8Zcfp^v&yaC)P-;48_2|N%%U*&a)|?}g?wtVI{n1h0QVnfVJqKc+E?VfbtLRpj9V+@x3i3AQSNEJaNN*zG{;Lt zJ9M?TwMp)oRCh4^+CEi^er(;88|<|n&~%2zsI9jA)bgJ86~~+O#vaO>gsHt7(bM2E zY*B0MZ8`FZkexvbMC$(w+w3Z`muYf}(KD zaYCEOS0uDbVDv0K_pTn^>tOnx(ogc1im%0&EXXjAKIl?!{Okl&U>B7&KIVx8k`9Zo zhw>7fuDnGChH2-)T{`kF#>U`!yi3>GTE8tr?N`UOS)^vO+5?k+Bcq}AEbGh$V`AX( zd$>8;ZlZQ}G4F1yQjatY_E)DbHeR&~LDYZQT{H1UI+KiHQiQ9)heiB@y%I_you1gH zWW`FR7eRP>_rg9`uj79Q^sl7fQ&JMh>+9xMf6p>9Rro;YCKAgM@)OB7^gSMPd z9)_N`*pK`BjDxNJ`m3s{mg7s<2d9g7^25{pPsfY|YEp|zI6O)rs(8U3r^>f|?G5lC z6|!N^+rD3f);li1^O))QG}rTsRE@SU!^!uB=kE%|)tv`Rkd}I$t2Olf!vh@juv&(J zrTz!x8@|^u_zi2z<)Xaw<$EKe737_43FKt+S+$f6>O$Z!U%aXbG)Oc)VX6bt)VZQw z7tC)Sp>UlG9D>*J*LShgrMqCzNx2eKHRfj}T7lFRE2(>I`%i!?q% zw|2=029$7%4a)l`&ega5q4b>Vo2{adIT@F8^NLNIY&^-AD_VJGmeLD0R|4CBh zL;jnQcQM^Lfo3i6potz5_==PMX+U_h71sc`-VDUC?LXFh7VG795gIsq)wdHO`L0rO z+LbNpr2V{X8t@BnC3Q$~y+*xTDqg7^VlA8RXbHc$u4tZfE{P5vUBVHUg;7&$Rh?oE z?E9k5yA0C1F_70K2=PgTULU?Vi}0<*8w;I(Q9qIx;ti=7h{9mBe-R|CN;|I0gy}RY zCbB+Z_X4-VVtr|e{g{TfT+6>qm=;gG5L!_i8mG5cl;1otDtRGGTYk4lK|2N#4qJ`= zT!^|yF7B;<_vSL6==rLnm#K94>L&0z2C%%0{cI|(!~7zoEKOvr#c*$bT8rLv*cpZy zJTT2}V;DaLQd2wJRUYjVu}=s>H&5|L0hPchsfU&}y~oHs@dd%8Z(o0J@xc;jU*J+L zNgjCN?8WNwfT018?TS~pUMzm{;h2G%MtBzPhrzJ|kRMkr@*!_K(d5cv#_xgiDwSm( z9gstN@>QHfJ?Y1lv+PFRpB27GdphVD?lGZ0ytJy0LbWZPZ6(q{ems>?r?~vHfx#;> z;$orZ-I^U@(X-wodau{KByewi%St@~875MY)udk5h*5ztVCii8MX!{+OwUy|P|x4X zY~VWFC8DD1EAUUxcB9+(jphb-C_@X0imy_Ey>28$>YR)R9mXsIB8yL>~9zhz<(@b1XctnK+tz^{R@AMPp#B_NH(WGesa1<6>8{ECZxXfJn7?L|@J`HXFjtzv== zu`G1o%;~~+Gv9XJ=lmV*8E@wE-__nB|9i6}n4lJu5U`FnHAOd9=(s0Pn!x$07$Vdk zK<(5~ziMDT*n2m4*gp&Zyt|VYHK|#9V1F7xuwglV&-?Jq3uT=kq@VXLGGhNPe6J{- zw4d3i)PG>!X1x4w=Iwu=$NmpgSGo+a{2$SW|NlhZXha@eDx#-Z4u${4=EVh*{9l|_ z$tVj17!!d_#%uuu+U{-mll~`?Y8?8%(7)1Xfnf65|G@6eQ{BV^+aqX`2oNjc+LDI8 zZ9|OXKlKG!nIwYrxM8vE^MA2{|Cdf28UZV)*ZY6qcK;i*tp7ianNrapqYZlbKTV_m z*c$&Y6!VGyzxlcfsI=^Q$m0IC1yegC!%h#65uyfp*Y-QKS5n~rh8cfOh*IzR7yEg+ z;oluKg=DX0(e*Oy0u__soz=NZbMWIX=Ib9ziL$)w&Z&s}<&pM41mG8B?CE(j_;|0# z2%&z8o$xS0q`x3=f@h4x|N2$E%3a(J-#7e!-Pg9Y4O5cp2|b@a8Yiw(zTeu|@H63< zi20xY{xbpY$x1T^Ik!m?wTP>ls_L&NyzZGIh`-s>BguM7SWo{Q`((S^p@rO03(E2p zc2M+-#I5H7J30|pyW!glau$xm28>(h=ip-)zHpk>Md{(1b0;KfJp?3;BNW2GQz`qM z+nBpOfFQ-d%-?jW?v#D&_4_0N`*CzBo+`WJ0RO8wsLjopiafb5-9Bi}r_*~%$8_ED z&%e8OmSAne-%g|BrI6kkP1xD=`HMGiIMV(^)3Df*2GC*5{c92G{x^Kuuann){MBu@ z<`X-u_~jgu_}PYBwb$vIW-vUO+;%rBQb${Rvk~qOkXv2~u5{ZFS%CA_)Yh_;nSXk? z8PFWM+!qikUZ1>(^khS)GdD)M*RJ@>Hc`|8#W`$Pcw<_3ZloyWO7sdQXl z(T_BC!f|e&;2{jG8esgp_FqE++aDmec?j%JNa7*XZB`so_`>Rb+-R;2%J?sQ_U8fQ zF@1lq`GYL?+EB_?y1_O@Ki9$K-RU6gLCTa?3UBkW^#Uk zBUt4crgkRbd!p*$@nzlHB~mnAk=i3`UCZ0kH?&y0r7|5Q&LpI{DW`4tp5eAgycMKWi#z242RU25ImF>52njh#K`

Xwhi_gDo7@Hb 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

S@b;9i`ze#fG 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%0D=ie*> zM$ZHR6e@PzCf)wGj%B*7^rXlN+&Mw7@jW1bv}Fp%hd2;&r0siFR`I5+!fI>1U7#y^ zn{TF5fUeIEhOY$y2mmiMtG+2!^Nf>aRfxs7;b_}g7p9BV@d+)FYva9Y+tY4`YXD5F z?VTZQNEuQ=07(VL=0-co@`H#N7 z&i7OJrwh(-!i|6VnD9>a%eTjw5}i~ArUDtP@M0yCY@68<(hM2m~eC6v`SG-+}qur8iJXhL=S@0pZGART1CJJrne|?WAeNW9fKX8Y{V^NR-$1L z;!O19Y1KG?=e){qykb1&2N0Z@HXF|s(rEw};AuIYfuxN(nwORbVg|p~<>vJa);LqC zv5>=QU8fCQMd8)cyP$;31Vt=IO=g4z(@1oO>#cry^GS;IjTM;|2Y@b%%Q67%1{1ZD z*&g#xu6p+0P&t~qAIT4f0TDOgtZfEsgH3GS zIYU$p+y(V5n8)0Xk8hAGVL4nVxkg%QE>>J&WqbKja9KiS|03)PqI{P3mg}AY>Qnqk#pdakI@8%7{`R>Yrm3CpS=|`2| zhR^NPF_rlUFz55+&&9CVx276Vj#YRY-I-`5a0_ zl8S9LxnCCOP*XpkBAdYu{K7H+gKhHk+ZR}A z*SytF48HLL%Bq0uw+g-8juXY$`}(y+j<1&3$jdiHH|U^ixW6CWl_^`dP*28n!B{8n z3(M{wg)FXh&s{rRi9u`u_|7 zhc5gSXJd7X@kZwtJ8AI<2zSfMVYct=4f*aYMZ}}cKN+ofHA36HuCTZfOt?_c$bn~& zz+j|kEf4tljN8G#7(Wh!C?9Xa?LUgats|KYKZ><1I~fReXN;l}ZLv5oj|-~<4>DYV z+7(0+8w3W9$H z`h8!|00f-+|1l}jpTV8SvV~2>NZHi_zd&K-fr#HIj!V;_qHt7xDb=I3L&X#&``Jm- zRSsMb5@p#r4S~h=U%r>+T)<%|Kk#J<(dvsN-nAhx&C+wP4iB$a8)tqfH`r_Znszs9 z&*vMh@qn^M5}vWluu@?u zUQ_R3enORU+0U}j6vGDeX|kFl#pjK52ypuw*_3LjLwfLEVg=AKv^qh zpD}b~eK8Wb>ghEYaNq^)BB2a@*8Ia2N=Z9U(BX>3sHstHNo77FjhV9W+a>iU$JW}* zSMz|dnFqle;=*q?_!)VBZY1~;YKNEf+#gtgtoT0#3)0cTUqJ{BrL5er7_~H!3b}an z6}EcXs_lW2i%-}u?txcimuaX^=PLLm#3l&g%&`X$?quxk57Ifz3NS+Mj;DazD!;?v z&%ZI9w$apT?+f_cOVCtt0mmksZp^WVSxuzx%Ln5HzLR_T#8n}PfE^OVeC7Ra+Z^X+ z{Y3`_M66jEo%Xje z2=Mb9*SJ!-+#-J!J8E)IrS0jlv-6rAD=xc8x(|M$FP9TYmfJ0TWHFbDT^g4}24vf? zSOqRRUFT-F-vm51(vsYtpE4TNItNGp4cveAy$a}(u(ECLqdxlE?PvgViLgAT@v=yf zE#*9hUx@yM7KMqv$FaB^aD_nVC*=6*WC8Xl<6Jj%h@qH}UGrx8g&BV3Koa7X#kUa7 zLumY=RdVxM`A63uZ%+^74y;d}%F2#4wY8QQDM_L#j~B<{@vN+ST(hY_@a`C;gK{Ip zX&XSgaEa#{Nl7tIiX1-<`$q9~{4yo`GWpx9i(lC1pnMvUc8+}>i~QzxdhLH7TvQw8 z8QtqxJZey+EOr|Lyl#qO{Bb$DZH&(@XGHs>`48t4JyK~(TZ*~lw{-cz+ft*RE!GP( zN@A<&K#FOTi6}fEfUj9&&dVfbztGcZ&G4V<%0%dVv%3jV74S1iKDA0yKvnv+IAN4Q zOo#-v(tfedGjvsGB4THV3iu~d7802CfQm(l4>Pzd=@H`DJmOgkk>1t46hVj&3GaX+ zhw0U40Ze%Hb}UZ1!pinjEJMgQgXxv3j)jpMnmWt#*j8TEa`BXy3}8sDpUY*o1R6Hy zYyk>PiIh=bpo)OnvHT& zJ~IUY!@tZ*T=jQ96Gc;y*0$QTl@Fh&3*a1Uxm|_*rK%KTG44Tq`)ve`uF^Y}J*y0m zStEim$N^BBYG>C|-qT&X$4=|@az2pf%n2kQ4{p0Q^t)PJY!-NT&0|Kp`KE(Rxml2zZhY?6X0a!S&IQV-D&=W0{iShVAT7Jt#9$ZO6Q_fMyNITMk|zxaSO zz8f4M&p0${4K@X$%$w?8EHK&7u}1m&!=Zk5C~xiv@9JM^ z+FMU-?t&iE!D@lVe}RJSaA-2}uydLA4E^W&<=wM!VjEl?khL1QDB1ZNuG0^fJ9AMK z{C3?mDcrhPQl<=%uUh_^KMi;}$HLgRQ6VpnH}@B^`*yw?T3JA>8;E@tiyi&h@N2o& zhC@WnpHPLBmDTU`0`kHoPy@g~(HoSibou;^cZUkv9bGyC)VwP6hSBF{E$ra6Ek23` z5$O6Q;)$kZo9DMZ3e?dOZIbY)ZT<7ssH*K|Mx?I-Tui#OqN3JaY=Sha>&=|X%TQak z3cKA}c>5~0@u_vap8Ch}M^`4#Wm3+uFe^{yX?V@hNpF5e3o_iDFDj$Lb5v8f#c;x-P~f&~r&5 zDA&{aad)s+Pnj{UB)1Ct2ksF3DHEZ_`?(w(|89q)pV=Er{>+@AE-Cyu_tc1WrMc;?$?7~WEbVkPFpD4h*Z5H#7oTcmqa^1I=0^Tg zY|%8jD+7)N7D($qMjpW77(D+ULf+%cRiT$1g7>d(j<9eu#kACM(d-=N7b8I}O`HnY zxyEC}{(xwTo2AydLss(Uk8hYVXGt;Mz@Nm(muz1;e} z4fu-Uf3fgA*h8fs9T$hdwC8H0vH1RT$Uj+!z3*$kcrio?Z!{|ot=#(G{)rXvQ%0FO zG*~2;z`CJt54(Q}!W%V!v-;>30RV?VfH)mm?Rd44DEH9>&i>km0`gTEx z$H`uWCunUxAb^h9um#=wRAJ~5XxYjbt6N%yX+v6Og*>CU+<65T0Vs~VQ9j(~A=KGV zIKbw4oZF{25EYH8sqklV%HIqHowwfrkqj-WZSS$<{p++G)o-7lbrFz91z3jN#^uky z8WCju-NqFLrqrvc2AlbfIidxKoY1Ke@1Puz6PP{Y6!tOWtkABP#QOC9JKVhBJJ0S z50*j-^~rsra{FdW29oak<7`2Qz2je~{r1UMVC=9dq+KqwyVc#?Th_sg@YTBTFgiVY zdb~#9=dI0Gw(aQQ5t7FECHqEj-iP-F3gO4^YGJ37_@aM-9{8ZPVCml4XCHp8 z3LzZH?)96$FpTnX1R4xW_dniaC~_*q`T^Z8+-o5Oh5wI|*F;&u9?7GIjia83HQ$FG zfx?0Do?!DpDshxWpj9VC`O7Is{L|WZ76AsxQqnHyVnM)4o6h$ZtQxD3tEUb&-WIvL zao%ycI-7^ZQE2}uSVN`v`l-GlMC9G`>8s$gXM1~dww%6CrZ_1KHHZBRF`&$@?^((F z@8&l)*T3+)e<|&7owHQbsx0JayZ=;P%Tj+yfcfE)l9vSj`WwZ6*)cMXBU4upy!nz! zCU(@YWu?Qzx96T&uMz~OF2$C2&_Id9USJo6Z>9NNedVGb3UxI&^8&Kh*kp%rqg*=R z1fs!X7I@#=BJbz^`nAR1O`WWH)Nzv&k*n8$lD-(gkxwjsAL5m6PO=hh@;T&;`~<&w z%K$%5@;pll#nFj%T0r5i#ih!`D|e=uS@77&d1CtuMYyzme}Kb<}aOv2*Rm!k_xJdIj2C zKFoLV*R`JK{F_tNnV6x4Q;z-BxI*ufZ)%_))DbEX+ze8Jd-nE(G&F&<8FudOG zH>Re9vG}(RI%chEe#Dosbr9XRw?HN~g5Yf-;%!G1^0Hz84$qOqJm_gdmI?#R?swi} zyB%*r*5!4{QHUBnruGQzN`i^oR&_A7kLBVkvKe5FjohoK$}8NYlnI&5WU`D1s9bh9 z-T2=@O*Bv5jRZ5t2gHEkHSFU=@Jrw3v$(H-_F%DE?Dueo_DC zpJJU;o*M)GN*REP4CLNY&+0=}&GYJ2dU~al$&li5Ss$9Yv?F1~E6aWB%e^rRs?tNR zlH$`!0wqpHD>Yyg-pD-v4i6 z7Pw5KlzBTQEaeq_v)o%q4zS#fllD(F}lqx9p ze>`8YBaXD4TnJu|j;oF0b;-g<3B!RJ-Zk40AnXyX}r2S4%r=zA0qgisYHqH?k! z1f-x1R@U#PyOR0|IBj0@vj%j{4V}YI#U%bnVnNBw;I2X*<_qIRKDU0`wwqKk$lyn` z(BEsVVmDdPtgeUq!Xmc5R?A6f01m6pBl6D6TCK+`T|?EmGVX z+@(NqcL-9XxVyU(+$FfX`@f#|`}f!f`-F_aK}J^Aow??`<|QRw)^F`E6MyCwubHu7 zg6&Vl^*r4#zU~uC0ti23=zB$0>5_{@pN+Y37@oT5>a8p?Gz8;aP}JLTO2I4=KF9c% zoBH<3qnJKTdbrCN^|z8cRajS#@^$DfNKo+MY#oW0(i#Dp6{cuUvxoDR{bQBQBNhoP z`h`v@W6WM}z~)TX?f;Y}TkK~~DF8+BKSeE|AEhm+;(glRCjQ=@jgY`-kkA*NA$*A+ zsCw!huwS2(Fu2kjoLKyhECvswlP^e6wo( zWqF0>y(pse8wzP@(Ta$}z6{t2W~YGb8$>Kb7lpGd2N#OdH8oBvP0o~*5x5^zdZ1_d zx_j!Zk-x|YyvZ}Z}omuJ1m-^q#ph-Mjkc5 zxVYmkMD;#e$7^1`Ok*&#RHU1hL`nO&EFE(bxaRN=c}0d@@hS7Ir)=t_D_tjMr0L6q z$tpZ!j?pX2c^LVl+9uN%=@*D(pr8i~B8TP6(Iw{}&-r?LuqmXQ$We!pPeyVduJP(2P2HSW3(>7)d05I7raug z$~*1-`LI9JyAx2sR@(H#*In41Z1^o=q&mE_;;xWT58NKoSVu?Kc}D2{derffW$e!4 zyKnc|1<@VYZ*Vrdp?3CMp8JVzAXIhk2H%mzf3G`QLd+kG%=i|3!-Y;nk*rbK?F6cf1uBXMDX-F zGif#P?Xc)(BY5qJ$`nqTPBfpATR&IWPB}*o?<{u(u*#0#VSifaD#hqGqzYRt<-hr> zkqQ4#tC+?4MBpDv+qV=m)>XrL4}f&=BU%ol<9*g@p_~hs%}%Jqu=)=r*bht{!?|1r zPd=v^zePmEd&N{gBbuV!Y7h{$j#(TOMF4Oh%|=qOURO}=Ib?f>t|wez(t0BXwSgyH zG{rr-HM`#s4u+X#dq8IO$L)#1nQNARJ@ZI-Ml;)S)rx1kWAlb9j0<3e8ANS4?a&N< zgHMJ)knB*oIdHxZ+Kpt1XEFK`KaK6c^=J@QqafQTXb27A9-(^P%-3Bzm zm1)b{#XNgEBzo=)46Z*Wr?uYED?$WyM&@J*4*331A`^bCpw1V*$f-kA<-Y=#*Q*$t&r*}>7sB}jUdmoXXe*e(oK zWvgr#z2$XDF?~1s2q^Bu>UjFDQZ6fuPbuUxxszCL!!y6Cq7239++KYAoRbl4wMFD~ zjO^q3=olPPiVmcK7grw`4PTCV{eC)(UAxe2X88og&ow@nu?t5EOET`>Irb-)s!aP< z#pZ*Ty&uMjU-n}=^;)y43(E$?_txuxX=jB46Qe?I&)VImX+*h2oMA$P|C$L3Y`20V z_1rgZTJ0e~WQU!w1sRor_523w*0^Wg&O_AeogKT9^CwxSbKjlxdL^v$HtwFM^G(+? z$G1vJa$M@qtT3t}T6>q-<-qwWuT0md1bge9u_XO?bV@2IDDJNs+ROVBU#g?Ru3|AP ziZ6P+TY(g;6WZCP#W#D4rK}!Q-}olhl;T*H;=nL1`+{xd6c(WUa(Y1WT#lrU&WQ6`Q|U{=g%lpfcb|p;~5P0zS>mv=**!lu1B}RbS1Y9S5kd-i#O}#GZzYWRJ<|Do{BDt^v9^A;#z%|b z@;j}yc^%2NiS`Z=@LHyiO{SN4HoKg{YiN8a(QgehmX0Fg4!UL}Jz-?o0u1?rGreB| zOYI8REHI?h8@!9>igf2JSym?`ie3}`B!GoPZ6Ru{@`OHAg$?Rv64p}&i{ zMs%j*X7?tPh^qqUc{!{b6*pf(5PpFxvaL<^$LTaC#Ze}m=?mRR?RWh)Xxhaqi0UVSataM&}Dd>S7Cu%h=HQnNH{gxPSrmih|9!=q?Q zMd5oA;5tfA-LR?dJ1B{xo=A8*4N z?_)2ISMdL2`Y`wZN|@!lBHqq+ygS3zpPEF9^EO)1`1{kV(+BKE#$c*UX{CZBUmf(X zy+1P3pY=sObi2xF7h$IpoH-_N&yhKH0lMdQRis$B>sp)90o`nU9J5=IY+GI}cs2Z9 z_><1@RzIVjL>vEmkqRjSN>tZP27JrvKE=)VEr*~nxan0>+dzx1#jnjxVYMUbcVV?B zBAvmwc#M-B0c548(paN7K+XZ*0^lI>UhjnPi#H`v$n!~+=`a!&S?30g-1&-&&|BWR zXaVjDI*dw#r&3Lt8x`Tlc?@En9Eo9P^W|J#gFVADEqEvb)`8!ySWqOMM+G0>nJ!KN z(Fi`XcEe>#ms$Y9&Q+u42y^VMao+=_aJ&rUhqU6IKh0G`kD zp+TkLI%W2y3mU0Q=wYr)$wU^-!EAMJYOPh^*dn9jGgl?6i}*@Y#6pgCiZKL2diQ~7 zKeD@DF+@A|XAnM_R7E-ygmMIYFb-(?E zDZs+6-NnwAh3AVN$v^$Sv<7@@wH~Z*ZppiR{lt`8-EGppVq0Uw9v?oeaNby}wt)dH!v^NwAIiVtM5IfI;%(bQ~A3uIM3+)Jp9NVR87N}&0dxNp7hWurl9b9&`lQ}{tl{h26_>#Q?GxzP1{OvWY2A$Gup^d z3@o$}EeKuH5G6%~MGqj4E{;o_rphS4BJ zF>n1bnz<-mGr-jrYklDHmrCpGE^dY$?j-Y?6=L2|GOp7ws&`KmrV!wpv3rk>mo%a` z@2j&gkJD+Xe_(z8X|1CV$@avi(&Et8d-Z(}ztaiPQO0ubPBXbq3sK(@mE45jGjeDX z$yZ4oV}2CJgXIW6F4Odd(%X#$^A$<-4Z7{9dWB34yH(SQ=pD*lZ!5~3jjjF5`Kqb$ zH;F^KHPcEXVSULwV3x~_N(6e<#9hmz4qSu;Oc!~--5yRyxN+U6f^;WO9)}I;6;rg~ z4)7+5&n=li|2(RyTjZn%H+bLI8KQSSdi#+8MCT@U>IRc$Yw_gc{AXy)xKjlSUZle|*Dem$p297kDq_oaVU-~hv5mDby>UK+lTFvo`s7MAh51#So2 zCnLtiY3%mfJFzo$^g3bntxvTv!m1wq`O9UJMq{c2epGa0dq&EtT8F(LYC;(b<^j z9u)OEAiGFS-c*>UaVcPF9bau`Ec#ykL*oIhLVlIL>1kE(Dv#>6F(Ww`;f0`!OkKZ^ z_Q1$L&cp7&EG-Dr-!3)uoNN%*{2&_t1`;O~-*j#52y6WDI4~^CxjS1zm-@BA{!g^P zM)RDlVqC8-X7{&FbcNsE6<~W~Nb*-x%w|>%gEC4vwqK>TR=d=(#nD!Jcd%Zd!$uZoj^xxshbgsK%!&#)#%KS z4H?L_en%1usqNX*vF}7k7`TRUkH}B7)oHf>01LYLY!7h@mehW%Dy-Bzy*^%2!f#($ zM9_GH|8RW>{WK4tuqEz7Z4KA-BxPkb_g?Ru(6vI8r3v!%9JAJ%*VtU$-K(za8l5Lh z;ohtq{kG=wS@%b~8ram_rt=pY2PRAQFQm!A+D)~cUA^A#j8dxZ+@J1Ho1C9BIxVmH z$L-PrL3S%m%*!h}#+N%bO>%!M&pwHk#0)fz6f&GU;pnT5rV1o26(tsLS!Vi*@MkX_ z{gywwIPewlxIOH^qMWP+to*jVPDUIb^|<$=iuE+5cj)HXAlm9Tx?=zypuPbC2vIR< zY5a*Hf(qYz8x<5~IuE!H16goP&xy);e}=h{^(KavulWt-HY_>vIvlFMiURR=O_8Kn zKLwqfCw8BAMl;ye2Sj>v;+E&8wiPYwGotagNX;Iu8jswB8SK{Dp8f6IH%?YinqIPR z)41F?5XKNyTCrVU&SGGdw0pCq&6qu(!(Tp8LUOL#({w!U6qCCRGvAmqx(yHkVi(gK z=rS*v1CED>~V_ zK`ap7QY9sWhR;`7C_2H6R#=n$KoNF@PWDV-oQNs`2~T=TQIm_tt2qgx1@*X9RkbjD zx>bYw(zPO&YzhBsVG85KtJ7wI@8NUG`G*W zgT}k%$5m?D{43=-FgZ%0*L%Lz$x64?ohs9bO)Di133MKkkrAdz#s620(Ip)ZtXyfH z-_+8>H=OK!o52=iN<8BN>5{!X!s9IT2Sv#}$7s#gDySdC2iYOH@*H;BJcm-KqI-*f;j}EhzU3D{}&PYofP$Q#f(GE<*1P^y5g=V7dAAn^|kC+od|BJLve= z$HUbHFqeM0^p_HxL)}T22%?3q&Pv{#SH;D--C4 zvArIyjG^#$n@D@donQ+$mN) zSBNbP^`i+bTYVW96P;YWZfeam>dmxm4!dnUP_ej^UW;ypEWXB1(0Yg2#Oo_lrrqq( z2SAN^m!(Hat_~mg?-?sXbE0sV5|#pI^Lf|*UmP5J_dJy7jPXNKdGN-@jLuPh(gj1A z>CCg_Y$l@Rdzh##WH5DX+&^kH1BgZ$!q$`xzRurJP}TpnuTU9ZX#06>{xKSfq4qTf z71mAST85CMs?sCLn=YhaQ?bVuftfadM(g8V2G%K{UEG@7N!>s%GPY$vL2|5qsQj12 zq%lA=p!pn%#Mh{o=jj;z&mqD5erJ@#Rk20VQznbzoIV1&7{RYxTt)@Jgy~pMl=uNX zoIM58w?xA)-&f$3#C2nT>gzTKfRoC=kR)yUsS4YT6~-^3_pyN#d!|TJM+Tc zD@9z)aPIJmt+M|IM#)>q|H~|7MP>WzaI)}-HI26^&mjR{YaytjpX!0z1o+krKY>#o zB8ReuC1uHsy<|*dTe8ko`!m&Vxd#>Atc`_MnUh$A+lBG$535En(eTP0Q@*IUieONF zAYZ`-Nj=H1vce`7f`$&2>8I&mZnC<)r%h@&9qSOZ1^6AwX<(FkJFlHt;E5gBy1MFI5%-3g>A$Z^xXS@-6f3G-w&k=BTvNDS z_cjo(c?Vt4=6-0B4I@qJ89i~iQ7VjyZ7^>HK5E;+2ch&J*2L;luE^o(<~xetUDA*0 z;x&DnDDLZUO>NKptB@e^S^)Z9z@V2)!nHOP^fvom?3nJ^!yqr|U%4$RuPi7psmJBE z#%Zc0yG>=s|IG&Dc42G5`(&S#S^TTWAE_?67x7ior*f|ajbfhiNs9EDKko1^Iz-l4 z%=M$0xE(Xfb%iJ5!D<7i#BV=>UMW9!-t`v$`KnJGO~STw-J1%)5lAOOe*kap^}BPJ zC<3;H_rx-lo~8WG=S2JOU;IG1-Gi-cgTE#h;-wsFOCW2AMA+}~iHQ32DWy+wzTM=1 z64LTk(F#RM!B%(aS^ ziK*1DFwE9MtO27X5@nr2yN|wl3W#GM{8N!^aQRd$dk|I8>~g+$Zd{6ft8qUYu;mZg(4Qih@E|(GBJyDgn{{1Ry36tB z%P)T8YrE8jv&}edlC%`)m8|5ISE4fm61ntqRrGT6Ta-I_J^j^gxmfNkl}e8It3m_* zIMA2LMkuZ~E6*zF3Cxh@b_7JN*nJP`b+WkMxWC+8-kIi_qCo!>#X#QpkM4z55X!#s zEQ9llBJ#SP54j>0YhUZYO7%4Vcjc$HoVJ20S4;wjL#0dqMMZokY4!c(OyYQ{!*4`j z%e2ZfxKy|J9;eB##8x=R5_>MjzXI>1&c}pLrHsvzqh4seDQ@RX*kygdo-W;0;yG?+ zgMlJqIk$kg0uKO)&1#>xks-ymvo)E@ho1WFlVqV+)$VI$Gy65|$~PkSH($@oZ@)WI zoVPeXTUnBeLDyi{r>3i7!Gv1XXWAR*==w>-dd6DjZe^=~CtI*KTX$6xBZ4?0yZC-v z-B$JxBmRsf`|c_d<0tc3AYZr2&Gvnw$OHxT@ZhDeq-4;2Jbg=OcQ97%!)F!N@1eCE2`r@ao8mx*k??{B{e?(Mb&!|jY1!6a zhrH7Pm5eL${o{ZC=*hs_InnL{;tz=riAK}JQxaG?IUSGQ7|$4{%6(u;E9 z_)QRS^aJ2$QevJ{F5ug`l0y_h~55nT|vn_2nS;LMQfV`8`eW$;_25<>&gji1{7DFlEcporn&wu`qca zU{f)V&{@PyOt{7Ce!;X@d;0r<=jJW1PcQxZVmLk8{+^z-*)d|{C-wKIT6{jM&Scsj zE}!(y5&%Cb=7wCL>aBM(sDM1<+v>PeO!-(TM6^FN+93CgoW~ zmTVjHRQ>mrWQJ9nb5<#wPKgV)x^Q2v&hD9q z?TW+m&Bp?Tr5q<&l#I}Fwxs{O5IszOcuC{F10Ly*iVhlaTGWI4!?Evc{lq=eVf@pn zd4y>qZ;A<%eqr82dDaD*zFccn5tG#c7cz_sFQa0BK{e9JHWi6!_<3{X!%;5SG!*^<6FL`nr@S0ujLC+0h}!eo*{uexcajWn^d5AXpm|Hqz`>LGYz`*LOt)RG z;-^g1E9Y@A53y@|##{5eWd8iUq>KH>;KjWjX;*vPcetLrM6k1rmPGCPx10?qS0+cyTZ%yuF1@rf2!!u!xh zg7^Y6?h@n~#R}p<%)5iboV%N)({DB6rjga2kE@$+bosq~pI`jy67DFtS2%{FL18;5 zX<#)Dx3oW;-gmO+#SD=Ta|Za>?kD{5OsXFPf+By&F_aarCrSDcVi~U#_mSxF%g5=?x8GX`SqN`37ov zXMcef)p^Mt2Hje@LC|9^RzchJGX9nTMmDvJ1Tiz&mL3NG_dRn|+Ko3<@|;X2F5{~n zeg739DHF2v%)&fx;@&PSs$$t^=H(YYi^LvMD2erZFKbW2k+K<|+GPm$gYae(uiixz zQsTdw*`Y^Y6CESjAmzDZ+_V@`w&-T5SS$Rp<*u+~am*hSeiJSfW?YF2TD*p3Wb$3|eD*I=^E|#?`&o~na3oR7xMv0*1E0~IbBMbF z(>sov%MI&cdQN03fu5IJj3b;{Ca?qwq*PJTqFMwZKz1n^Igu=6qGCSypbR?H$K=Qk zE}GocufFzz3q!DCQep-SV-aUx5Z^1^1jutdTi7nqxU&5B9pp1e4Rkl;KGhE=5U(Jz zQ0JOZQgF*{$@!RlX*G3!4e8@$`h)n z{5uOh7F7y8HdztLpLH`Rksrk4ue3@hUQBMQVyV4k!TUBFBxX}Yd<@r9ZR3eo_wL4V z)can_6al-9)(nciF0xNHxir5i1@5sqRLH_`%jEVqUiSwV<8x#V))GJ#Aucf=T+j&% zCnT4fhrfRgVb!DY#{bIil%8MwyI>o%&OINq7g-}DuG%?Wqp{6X2(tZiXR1f8J&?4 zG5wnjxviShI65xDclk(MkymRmK7;xt^S1`?;J~QN#w%m{{>4waPyCFS`-51(J;Kq_ zvX~6?pzw3Tj+uDd?19BlXfZH2DrP?KMZ)L#4iz=zqRU^N!%G^tWB+$x?>mtAmh&2V zw@#8(iw;!pm!@A8p7UQ=W~3V#Bvpz+%pzvAzG*SwO~)AJdKJ=M=v}I@@gDnt59i_VcVH1 zQO!CgsaUp*FBf~|y(OL93xaNc-A+ldkPdD8J#v-wE`8Ue_tMNceOLly9_=_RGj^z8hM#wG&ylXV6;8gAk1o zoX=}30f!@3S0>1G7}9u&-&t2FV7uBfiboja_0wYMUAvN^OmE|55@C8lfRO4jk477f z8_-N`A_LY4$k1*0p#RQPiY}}zc0W$`VqE{i`Sapc5w&+ZTjr!$RKa~L@YT`}FjDpZK?@P{uc7aKWgA7VLHykYXgP1hFq8VDM#oa4en1lPy|So>@XfAPsw0!dk+v zeu``4OL^Jacdf>vU;hl=4J)@5)X3@@IuHCnpK z_!Akj*>Y8)60A~Q*)|dXJS5)y3!9KI!gj^o=sfnLZuw7;fsoz9g2w&Qm>c!{=eTgU z|5e%lySo3+-~G{l?~Ng8$k0Iy4@qq<%oDBO6asA>dw$&PQ}u+AM+SZpoFMibJGB?@ zwMbs+P@ke|Q6CK$uU`pa%0h$7EB2T{L-;N$WpqMD#2FnFoA6`to80VSm+b?L7FWVv zTlOloPi1`{_=7WB9q&JPoe03p-i~miWHAb#clRsVpHHg&7V^qQ_&d|V_Wat}ZLfUe z1oFHRzcN!7j849gkm%%d1^}~m_ z#wV*eJ4!(cRA3b@mc$}(ezs2KEvV>;|FVUoGR1kPM=s?(`X5%4Zse$sMrFQYoy((Jti zHtg?1P5#o0&|D|qF6lz-eRpGK9EfuQx#QaQec;27QJzQ^aDfj^C<|_XZsq!{Uf1cm zR_Jg0GJXS!mYN?c<~IXc)*eW`F?4lkRe|oGlY<$gGL@ z){D!-U~tcpptytx7;MUXVhG*9TMg5PQH8K{$=&$XuX`lx>cnDS%NoP(bSu5e^roLCUfK`QVZEk9wN9?6Udnxh@k}?ogKt*Xg?SzUU+yvNqVZ{OXxxyF)C@bdKb>Mc^nFO7 zxn;4d!{g!+bW=YfUvsvz9DTW0*i{#KP&mMoAzyj8@$%o*hI<-|_?yg=kLih$)cyMh z2D`1jO^mGZ_x4`|w^D@XE_&hQmSL!3Lm{A<_rFGzK;*YKFgItG4Msv>B^pbzHod4^%aFASY$Q#hkUU|MZ(=1e^R_-^d^Xx!*hr2Us@Oy)1$nZIc zS=U9@k_eAkA3iN8GMQlr;!$j^JPeN3-iC~l4R!2cEPxo^4fbih6T5V_5NAIVRm1Zpa%b)a? zv$n{$Jc3V0m78?zj>)XvPmLe6>n=LFrJ}U5N-uPvu>0r7s0JI%NJV;$Lgc>4Ov(ou zM339!V7V~$vx}1fn2^>HPucIvmfGqdaOI=Z#p@8|$~jZj!zL8XV%C+&Kh zsF1k)Pwea=ta#FAYX9$ApMfz`Nh#;e=lZN7Hqvm<4dK z$OF5lhEa_B?=S3*WOYYg1s-qy`b9A7$P$Nv6>SG}>Mf#xaAGQE<_!3gcN6C?{5OUN zHH0MS>AKlIdbw{A9uZ?z7X}#$H7m?{fzh+mc7A_B`EP&s;9v^8MdT~~?Vb&ZE_Ny3$PU zb3be_-zuOiE=~>X>-$Euz`Q%WI`IXjh6mGx_F${g#1pQzD^O#^?aBB@D#^%Ahowp| z`O9@VtAl<`2871yx}xII+D*||_1lOWFmtk|M%qZ(igX~n{<;g(j&-Be_6=w ziFeG+x0>y_kQV`3u&b=^M|#{Bj5w%1`5>85VeOxIr=v77pXCidaq^=K?)$K`19Jw0 zNQs=vB*(oxkT(&FX5h0!K_yrHl60{}B_k>s{REBY=t5*Qt0GBg$(dxNRBj*FPMIA6 z=AFGOh!Mo9<1+n-8sy`SE@$RZ#qDM(%QRa~5`j@H`H6<%%^=T+XJAvVR>>v>?(DVO6T=Zz7+^C7PXJ&=5M54J84IukySH% zuZ{4{3hrvjrR!Uji3}o2<~qR1a9#s8l&pV_Uht%YehWYiq z<9F+E?o&c+>{0dgVU3dB@ocK`&H|K|_SZYCbTr;!6n|EI$&S%cQXo`(5`Gnzzg4Nd z(}+pi5LoRmp8}@f-B()Z>#)609-Adf7D0GG>@HTi$YVY`LKv4n(wR*~=PF-S1q=($ zDS|+TJRNzs7DNuvq5s!vco~TcEB&%W&cRW6|1r~lpaDuZ@u(82B)jSKA9yGzc%KlT z8nSmMWZo?bWd>0S{Y*Dsm~yRI1k4r)0&jJs z*!^bk9NW|!*(Qow&z*O~I)4qeC(4(u2JgBv()sB2w67%`l2|3EYw!``ZK=r|`4qOg zyd_SLFFzA^(d=8@N1eGU9J+huG}N-($xeQ+23K1V1J$uFakTm;YX?^Q_m1TAI5x{G z#Bd_cn5hntzvIO&LcfLFB1@MLzLow2r?=b7;IE})HVxRg0uOXH*Ll~Mv;#FbwyF;O zD#wo`BsvvHO+PX%m~5&l08j7fYKzeM2E-S|DaEP|?>^z1vK4r1FGb&L+4gHbxx;Yz zQGe%!U+13B$Urj`f(fF--^(3%YaSID5}2d`xOMQQ>-}RqrV6_<9iHnbFm?fX-VIO# zO4Tutz}#ME!B|#;`S1&Z6alE+X_tj0LtwmTRpx5pZ8!L*hLG=3LUojN_>oEl@BV^FX(_4{ah30UR;)9q_U1eggCdL_8I$wk7lm?0e zYlM@o6t8QgP1NxJB8{0oT;W7;XMB;3y&AhN(0ncUX|nGNyrl{uMIY-XXZw?!hgOq# zVH+-U+`hQgPW+X;+rH=|ictBQ(g^sYtDs#dv9k?Xu$;wvHaXEtE6UqY~ z`<~@{xdnH(Lmr=}>vExu{puuXY3VJ3J%{rUX=(Wl@lSV!(U)guM_uOwg-qJgDrx$X z;@M{9@S2ssN>BI~i$5Q~1=$mj9rIU)fgcnqVfkwSBoD`7x#qI={Ab`F0U32~&eKjQ zKfQ7eHH&Uj=w9+y$3tE?v7pB)Vj3j40*(Ut6ig2flpZ?jeIp3q7PC!bbqOCJ2~g-o ztOLt5+vw;D4Cl|3&1aWqO12x>&z475h*2=fK0YS}6i)5H>Wyf=&hqwh6cC)um92F7 zJkR-TBR~fOz1@TQEC`T3M0_iM~m*`J$iR0cx6VUrZWDDjhEa9*08%&Gfx<|SICjK!}=uPp`+mK42T zK4WB`U76`w{WKE>to&NoSoX#8Z@226xn0T5H!#&lF1s4ztfJH60#|g*ery(H4-6~# z?Yvm>M52^-`djD^cqD|469>cU|KUSGEqCg$##k~z_cwq)CD7rK14G}SuY;!%hh$WS zuaDO~X(E%(C_7Y4F>^B+h)6pPpuCBTQ{}C@m2G#DmKp5`!)IMTS;f=yIgL!7|Ekv( zL0q2&hFE+OWrhcOsy-G^luG&3;bCDk z2ivvXOV*g>joDDm2w7an$1@mcY`OJSw$aI_zgy~4u%|m}v*ncEPGZqOnKBzOj$V5Y zD!6!CqF?RInL6LRu~!+V{S%HXSV$tRj?CWjz?y5v%@5`_u6!~Dk06!h>83NLB<}ls zz)gI`1U2~Jx;jJ(upwc0$DT35HCD4njpmCryzRknFeQ6?egH#w&J&fitRl|lhXLDj zoDZsZE}?UZcz61%dJe}A3K_L+h=!&Qxy;aQ)a3uoj1Ry6lql}OB*>`ESL*2sF7-t% zJ%9rK<~{+E2cCPOpso5H>wzM38d?=-8w=ODYvHA6hNBWI?Vc15NuVkn6H17WzR)tL z$gVYvzsmZVckT9hGFJmfhMcXHZWydEatYvICb<%nhW#c?sWb3N&@Mzc-Hety@mjfMLVeV@x~a4pr==@14%}uWWiTW za#p&p{G7I%$L*GiG|uu`eUkX6SC1+g0qIen`u&WIh`dhzdWZ6)UXFilM#arcI;>G9 zwrv@~_M?l7Yq@l!YVa(Lzr;gi=u*D_eLJ$`F4`QkBe!-`ARFSzI#Q{JXy6$ z>qc@c++O>3`}5Bv>5JK>jO2hLHwNHw6&zs9I9;X}wbF$1$MdDj6qi*W0chCIDK#pM z=;kiVn6&3<*Iok7-K63svLCHU8y)7Qv2O4e)iUY65^;x9+3UyXnerneZ)}~*aljPEg#DRNx8pIgM-1y_2@e(;mMQr#K{Xj5N{>~F+-bnuSGr^^ljXe48Z$G{E zuH5xnhtn~cb6tN|!SzyRC|I0w{Bfw37(gj_wcQ;1FA}Vn{7Y7^VpSQI4nR zLZB)2?*Z0L4&YS!#OrbLMqFI-@PO83%WBrfFD#6B*JA!3wkO@#d$%0TAK%(I%~-o; zYLNo=Lb}pVJQUp|N?q@Y{8Db^i?7;pf`zCqCFFZ6v0% zAVfdjDrWddJtOm&8&18Cj9hIXbx*n+e_U;FLm~C#bKvK)UE;R+Rsvgcb3LmD*yf_; zt6s4BpH#r~7fz=HVIOE^H5B4=*$<^t%A{go_y}Z%^Z@2$S4_>U`fWihdLHWo6NV>A zcfURV^*@>|E@F=g(pqJWl;s`dSbTOk@9VSs*jn-z5B+ zuDi-$_h1&V>q!5Ut=Wc)=L(^V;gpF*lL+$Sf;AF0X3YxpnFRh2l-nL$8X|S_LP{_l zJe{p;&*E4$ePlX?g2EQfY#y0u7Kt~WD+y0$b-OA=+M2twTA96KK&uss41LJLlWsG|?yq>YEs7%gxr7wpEr#HTxON7i< zH~`q0!qHhx%{y^PNvfwo&PX9emU-^$gCyX$F}Tg^w^OK%zlA2Ou-)HFV_w>ON$^e; z5*8ZS`L|yQn;0KjHl47G3uiDW=Fl>8-Hsd#u4NyIBUtvnd7^2LYx4RoE+#^>WL5yPfL@*Ut=RT>%F+e!c76xmZ7Ub-`&ZJ z|Mm2%AGf!*K79_Iby7MP&jmP!A>8649(t}ivB52O-yU>*6#--1ScLl|_K;IgfP*r@ zsoRJhgg=4m^U$d!0&`PW$Ag4idxU>{Dn3i3Fqm**rK0T7r_%O6JWU306~_x&&=V>| z$fq+YbL|TaEp27XBj-V#wxkpz#{(*vfIHL8hBAP9SL1@{2&FC#Ti-L`mzkKE z9SRd|a%_FvtrnMJ=ukeenjXc zyb|qJ9tS?X$<9)VOM*MDH+0m?RK+>wiRxa`iAX1|JLo-;Ypn}$p>!n*aR>OVH+$pzLN|9SpcxcEe zx`&Ug<%!i!`}Px4C-^^r4|cz#1>tmsbf|A>?Xx%d>{8d2kwy}3>als-W5|>5!>x64 zvpdxw%(5JZStY%RPW&)9dw}Np!ehIH>Px9{fNR`r!%rTWN$Pw{=8s7ppj2>D;bI2!VG8JgAoFtCQ~GtNImJ=K3&Cm?36lDGXoI!NnE&KDr09+)Lfl)lENa#i*}V5If(0cu_QX0}f7 zLq-=05UYn897Qq(ub0V}Rp`7#rrfbkNGcz=>f`^>o7mxAlHLesYIV?$IHA(=7_a8~ ztgE#jihUV5#I0dq;#n04bJK6@p5KLL{?#a7oo}!RJ2|WUL<#3|dji+uNtmsnZfQx> z_o&#>B_S6R6LYS0w)~hgs`Ipdv@vwi*V%~;ptzktOSk6n3i;~r0Wh*$2bkSs@)|ASG=CNaj~Bc&jh>o9CDRTxjK(szFL$_Ui;#SjYfY ztr^sI>&MB#jFq&E%qU6@$o-1o@fO8Fv;G+clLGWqsD`avkeBw8w0g;fd8$I|YbZ}<#(w_gS0o29=;?|KIZYSD(vJsOU zcJFOQ7TH(1*{NCY&FRtPN$WV3HsIHv*`ErMd(y| zZ{q3V`q0tR><`H(C~Q03ik!E+vLdpXB5QQJrq= z@~Oo>`iYX0JL7jvS1o8xGxAU?h7Q!* z%1>ucVS?_DRob@zi)iqAe!pQ%#_{ciuYB?1>4G?5*V@$kf$8Cu{heBox)hKc&dUSr z@lK37o!}S>2|v`t0i>AR|Kr0vNQanEkzRl~=C|_m@-|0O!1HY@2Bhu}4RO*yx+uJNauo@$5 zW@oNj&|Q+6T)2#}3l3G)mM-g%m9Rh>56c8TCkw=%AcY~oeLM8ZOivG2FI{Fg*?Y@um}pi*BInjO@5#1* zF(s?-rqN<;+jel?@VW4Er*Mc}W4Ck(@#(&LWPh=i_~A#L@ol@T1#tkNNO-alj1TnfZvd5o zUy=rX{AX39O}a5Nd&w9CvoU?)K(yH1q`tN;*6Vs{bSo4}}{45V$; zm@kAz;FUOBDn7XHU)26nsZBIjmAX3Ne=8A&C$>~)nxH|`G)@|`v2ELItVv^Ajg2;#*w(~0-}C#wALrX#Gy9yq_j=a4SGnNt z+)6ZSiL&(x;#+y!5~Xr#Z*PH1lXrzS|JwgJ-U#Su|HnZuXPu3V`NgQM$ll%>QQ?e5OkJ(lef^W6O+rDkbgsSAQMABKMsHC9 zS~23a#ZmMne%a5%h64};%`%gJ#L1}LIt|^WjbArgME@6g4kLtT2g>0xx zK&z~g(}jfu69^>u+fz<^16>Cim5Bdsz6qiA34~tBY>Y97WNiNN;~e zzq0x)yD5;*A%=@SF#H#uuni;@F{~HZ9zG)TyT(;QHEHB0*Qs_mm3sGtAXR`!d<|x-HvZ437^F+RwVI5x?e?MBJ~Bzn+OR@i7&XI>s_9Yh?iZK6ay4pk_9yE_G8TBH zfO=F3zkAMal{@)2^FXxv;tFDNF~V9X@hCT3gj%9g_&JtLs0e;j(1ko0hgK%vEk|Ty za}pM~CH5wLx^jmYRH}mhDiEgL8g^($n|PF~*-X|b;T|u$Dc;2@1mIno<6*4X=zz=_b3S?s_@-he1cm zFtW;qN2g0e&ze@pnPT;#>&a4-esE{!XXld5R+;*FNEbn~>8kX>$^%-7P78n$YOR+q zZdsog$B)VU6WOR_QE{oA-#q{MJ&2c+!jhuKIIhft?(aJ+di)aotAh{?r}or>Or*oQ z(F@}*&z1)RT)@HS@x13s44_H=`!3};if>#XtfCn|WVPBnL?iR@@l{p+m&P&LKu+ez zS9#<4)1;*KBnEvo&yT68=@gLNu81gGxV$ag#DT42b{5^v&?hVk>dyjtgMU+iSEn$` zxsF!IfAXZVu;St-}}puoq^ zub1aO{4j~^d{smsIOqLFXQGJ;`UIfjnok-Yir_E$xyE3ZfzsuN_RABtGtztczn2y^198~o(R{Z>~DCZ$*`43v!#zABU-8P*wXCI!6-Y^dMI z=<;Urg>&pS>25|yX`OqIuRM7A*YJOrk=Y(0HRdnfa}n^SJ@-D3-xL_3ak?5>x!3MP zLP)-0xsSt~MSnWAIclIQs@;lBHXi*&I|76`i1<2s(7OTPnNeY0pn zUpRtx;C7XaV?NmJYdVGf`iO+)Olb8EuWSo2(Uso zj~B06YtL^FP+8BatbNkr(}Fq8Ae+z}Ka=;ewUBw834>&P&=K&+r$R z?pj>8xl#~MkIi%fDbYWGk=i}(NB$MJ0JWrgDhFW~|YaH6|my)H{xGeoK*t*XT2gQ+~=DyxjhqNSs3Ujq&e%O$% zL+R1Au-AojW1Ueq%Trl>;5hR95T#V%j-r>y_)r4+Mc?W{{fa5T9J``PJy%u?WMp15TJ9@%fO(jLk+gQ0A`Xy(rUSQ2(>f)ca<(dZELMb>0CgPt@Mw zv$453u<0NEpGyPi0jzJvs$1%E*e0-?a}vuR9CJSE2dY8NLxR|ihU!4nEv;7b!dwkD zk$_E$?T>9~G$#W1sJe@@UrTrM;n_)hGM7XcGCFj0=>0wYpZD}kB?1J>5}<9xs?+8Z z=hCE{x+yG11Fd^_$oz@5xZ1|PN(rg^sA`8vGJRyJnYY7@P^bNC#^jS%s`r)VvRSQ` zreOtS=D_2JSdX4iv_CZE#3Tl{QstW+qnFFr)tT#wG)V61d-@w|1Bby?%1OzOSwuos zz1lykgWXOh&-zzRzKhm+)yDg}j>_)pmAEcBAF4M8WL-Kg{-(E%Xbs*%La$=ogKvlb z>8aa2H=Y!%qu*>>b>LVzW3lu&N#lU#W%$3zph|sT!7whC`asVK+oBoqf%ZUI-t>cC z4zn_5v4kqc*gF3Q6SICQZfd+wC_gjYb?k`NGyECqwCIh9X%B|5|KFEgv(f|%eExfr zeoiwtk_($M*7UmGXBQ6tK21?WN-?TZab>uB2uvK3W7us9SoJ$20SLfWcK}t>@BAQY z@$M+h&i!~1`#0DooEG1g(DAZh%nk}+Zzi5q!djOjSG-S`4xcCEU@L1cBoqJ{q$hxCw*-_q)xR()S>**XI~f9z@u@ycS_ksX71_Kt$1B zSX>z8*8Z<+GI!k~!EU}vhz}npuUvhe5kb67K-GPbGlTEO zxdR!GUOCJ1o z3Zr!EA)C{ohy-R-GjDk4vfrhHF1H%7#zyggdxU8aa7HAjAA_Cn zQum9gwGp0SGc%{KK~g^-D?-SMXNeRInE%V6ZA>*<%`fiOZTV)qhr37K#|%C^hoE^< zV9abh)NIl?Y!BMF?t1J`{6zdajH7`Oov4aKZb&ZT8|bljoz4k5MYWsK$&2~`L`Hw| z=0jO^Bn&oS6V8~1>eNyt!)BcoWp5lbi%TmsvISMFcpCm?3DYczn<%9l518(^rej9_ zmHga6x!dM^z|p3-cN-<_ea%2dIN&xDh7^Pu(c%@cp1K3mLL(fkD|n7a3?tPtsVokpdL!LTd+8e+iRYJ>0a4|occgzy(E{VR z=1P}pN5_9|>;&)UcX+`scxL&B^Crt!_YmQS?6KIe85TI4XAP!Qq?eAErPAnm&d%hCYP#kLS(jw($8s@8_biTd0cNFlJ(D$qTedqUM1&byygl;nweX zDWuKLmqa_*Q@4j z!U5z-P%5b7M?f3~FbcZ;)cq&sy&OK?qf-dS44YMcT_?xA3cWtB>OivJ>s8IhZzKU> z3Hu~Kmw6H(er{AOx^0#9k@?=uA+H$d0!7*~WI*k_OS2Z%dJAPNkOtyw03Jz3o!B?9 zs7|{p<9cB#bn=4F=S!klvAO#217m9|6T)P&3%o}M|Hs1)m%D`hKJ5iudt|GAvI6dy zaU8uJ+NcHpAgNm4Y1d-{k8%CZ%C=~!7h?DQH21x-VW;3_=hI6JC0k@3+tt4&N)>K> z?qj4)$D&@<>mjoIW;pv3PRq8z^xwKFbej_b<^*$p-fu5y+6O~q7;Vpl11hzQLx;OuUNvr+2u@m2_7QkiotxYD2 zD=|nMS+btRp5J5-A90IkOLHtuPvgQPVa_Gi@3?jXSZlzyhIW@j+nTcUMfARAb9<`E-y+du~>E}P*TR_>3yrBcq+x~Wh&(#){^nXdgyOkT~gbq z%hL-Kl>OBmApr+TTsYOuj`#<5<5&t z`~C^H`dnjXga{_$=GOwIs_nLuJPI*n{nYHL1OiOTl6-M+jLe7F?Gb===s zlvxI~e4?N~==hf4 z4J_i=o6Hx7%|qr~Ru(kjl^9&=JpJI~InB(Wj~DmT_ey!SKkjKie9bqKrlb(?{Z2C< zvc~VGRB&66 ztx0`vUzngptD6XEw6;i7psyGO*0pyHTkpds=cFxD=_Xz%_@ zR%-HgiOyjoudTgk6&HZh2KrWl08-U&;CQWb7ANu&4Xo&MY7xzPxpW>Zm&3XLG%&?l zWtSu-O`O>>)olr+$9FjKf^$F-MINx}kSXuCo%xCywV8Skm)H{e{8|>E1Q2M2;w~wl zRY?Gfjko{rrdP&rWW_&#{s>fx51J@yn*37}Q!&Fr+?oBoeIj_L*r37ee0~t1RwEUzI4w}7GbhOC#=%cM^M zXThl0-$w&&mkp_}> zlQk4x`(Gx_y%n^oqo&cO*F+8bVK`F^DTf6_7`|`NWY-02J~2T-BlU2Z_4`33PlDL> zK$;3a=s57K*>*i76r<4u1-w}&;5?@(thwEKbFobi*i{QM3EvrVH#zO_m2j~pjgZ$% z`puSTW-d#(;IPIJAC15;{*CxFZMyyDbHNI(F8rP>R=Z`gv9Uk;_ICF6goXHS5#w=9 zlzm>Nw=e|hIAbBIMHdC=#-O2Gm|h@)hbLqn#{x6f1&h{D2qA?73A`m$)zN%qdt4*O znffx5q$c_KuKN1snomrWO)GMP^&%=FUMVF5wjUN5 z7rFx2a;suW66Tl6wRSqk?xG8Ga`7ZYRD}b!j-L-y*QDV2<{VaY`rdv!+Cl6+JXVXlLd(2Zw6&52fcaI*dtxSV{ zG((|M2f+siXMO4W8v87Gfu*_9XgeLdEu!5Uwb$+Riw4jXB?ta-ONgZO-HALi+?O$V zK@^#sh8>}!Ay)VEsrva5;%Iz)Ej2oiK-IwJQXR&)eoLv_M5zYOSb7(p-`kM7z4c*V zcfhA^c`Ohl=#WE}ppDS?^|>V$vY%*pwzSx0Fi(MjEJG;EuvdKA8$b|zuxbWlId7Fm z$e4PGn|3+YEW)PI0Yo|wOU|@5n{?Bw|7$jWuk$I>zj4k6G}+;WwQ9nHKvmq81M=uS z9UZdhaWVlbwUzo>)<16e3{L<~rBThZsc=aU>I)l;aiyuIdX8Q zgvZ@bBfV9UR!49lK&`PV=xpSf3_qs~KI%W1<3Cj^$4(BDk$kPOuNrluaaj8YY<|nJ zFwf_G)%%NH3_$w0f3Ek38*W%pt44o;U-R)o(3F^nqIM)8OGJ9>Q@BwI&BG3IQ8rtz z2W^R&DP`Z-WVMXz4=i=7?OVgq;rYl@^&NZ2^pikBkMdo_Txw6V_{{pSPTJ_gFSHV&6? zfJ+=Ns&%HGKJ>CRlI6(a-#??M3yW!JIJc!taOmoE$2PEd-gUX|#^d(%Q1f|mrS)Ah z5E7Q^X{mtb^?s?YT}^Bnp@Yv>@P8*_r0lC@tWQe{X6b4>ta9nS!@(rsB4XBRn5tCR z=SZIN^^AQL@qPaBe7+s$9T2u8dQ=1qtq&h_!1?Q~&Ih5wK9AJG!mpLxPFniQsKT$y z>wY-i+Hw`UUHDFl#}{Q>*aEEu8*hhvANP1J*a9Y3J+E<(hVu-C)Y74Rl_RN4`h8AH zEu!L7CYi3;N_9VI!Se$$NjO&RbKa#=up}1piEXC=K~hoPg&q|6pDv>>8m0T*tD= zxlKRJi+jAT6TXSA!-`)>>^5azGU?Q59dladdR6!2RcYzap9or?WNZyb)VI3P)j0Jg zwUlU%gzi!$Yu7h1t5q!;&&edSqnWi@gDh8mT)bj9JC^DN(P`AUC!f~A&dN}gr7p|^ znC1u*l)wD|1GUfYgzl76{M`E>a#pO#8{$Q=M6ZboX1t074J+>Bw+4TC!$RgmmMT`` zbG&|cHQjoX*-ui(ab>g-&}XP%2ssj5l6ir=J!4ffa_x3Zg@iMFaPMt25v(ORfE?Lt z^}U?U(+0<`7+)-Dc4b$y!cgEp^*@T)A|jqPAATrjKhI*(<8reN$)o)o<*GK|w*E$C z?9AEVeD|KCwSegE5gA#4?mB)CZl*5zs6UN+t2UpVNxO=UPA+>uAk10Mk9z%Ia`ori zs>bx3vLEG91hypP+Ev~r6?)Af4{`2hlXcu;bUc$Os$Y%+tl2D&-+}eNFHpkWX*b{O za3%qN!RGQ=xkH;Z$s$U&6A@wc8o^1`hn>*2iLvS%+6%4uQ}~domk?ObsoEc`FY+MW za(SWWZA!_0&^cp#*g64-S7*6BYQhyTItD>Q0GL2B*o2Sy2ZwTgLdr80dciLfmkg=?8i3%}C=WzW|LHzzzN9d8O-eeMJG}Q8d*4i=Ey+6 zMz$vy&4~oYHBfGI)ouEiFYe~33i28k`%$GOWnHAOk9jf|72|u-+O*K*Ll}hSY|iCz z>0JB8by72DGFMs4Q$V|l@RM?I%opAT@tQVW*Y?Be=QZoYN zpc`ts4=*f19~DjM`y9dCfefN=4s}kG^gzTQ4l0|qW-}c92pB{D5g6kZk8g^9eA5j; zS7!gSYR6_x!m}w3D`34=|Hej^rF9A00tcoPb|BgP`~%w~Y(s59JT^+e?A7o`wNls@ z7lEYT-o}f2YfVA>%~jSvdF>{@KVMooROqyQ%4uXEr%-%}N~s(S9TR-m*p&OvHv7>2 zfVNO)IXDVDOKlWGkE#`X;rohd_lJKI%|?qQW#%`;)qT!ED3vQd=h~AQ3$;$Hl)rwF zLI4w3#M$!Z-k4zf-f!{4^|qTYUA`|ZAjehj>wSmk6ThxLV4rZ8SJVXdFr2GAlp@*e zkYWh9miwVM1o-VteU4Y%M2~LyLYck($4R3tOB8*f^u5O(NqN$wh2QtL?g=cshl=>U zZ+DG_;skub^ZFC!s;3Bcg0gf^Pa}<_R__p%IM=i}OcNv#8hLMQ{LD64fOP%ndqKjo z|Ga~4IafPm-*aWWxiT}0l-=K6aI4~NBA|1!=)|U-fbIVEl>ERDMRTH*xtA}`0S3k& zMq1*_k402C$X^n(h4ab(oFH#G9cTWLpGW5OINvgOul72)?Ji{%%(QJ9m!l)l13ysl zmwZg73N+}>a_T30PqUXr!|*w+KCQlqAL(8F@tb$spMlD}vJ$Zq+Z*e7O)8h86(`85 zS&q+bb6=Cq@<6-r*5fb_k1HPWY~+w#6_YWNKQ&E>XH2VM)qL%-*iKb`qKMC{swA^J z*y);x+lt^jK>4FmMt5R&+G8ryZKD_?bPBw%%)15KQ$|GCd}3k>WI@w|VJ;>n0^K^J}9bYhI^in z?94Sp&`@jGv1k!mL+)RrgQv*GMwk>hFpWTinl7Ow0m0a~P`oZswvm6v`25WQCEGdA zDEXVJ1PcZQPiVEeR6hwKB}0t(s27PYY76pD6nVJczH?LlwD4di1*lO7&Tfhv-_2jx8w1{Xc7|P6~ut%L3RN> zoxeGMRH}u|wM`w;VGFy zekkw&A{2AST(*Lg=o;1^7X2H}Bb>i^;a&@z`j^~`H1n;POCsy{IQeYLp4$e0Cxn_$IZj)_(93n^` z?69Tdo_-!@x8DA)2{(`3=_5N`@d}5~;_fRo?4nRjH&f=~rAHZwi?wqL zScnOi8m+1a#+@eWv3-QakAIT?@(h2lk~knvFXOImd!^p?x*_SlX~%vj>mo?*!1FRR zP-HmEO@VXxr?bK2&qR{{wYbKJekxaFoZ0cbH^XXTBI?d>KR)2g=FZ;Umr@lo3#il^ z@p}_e!u5dXXR*e{0cK*-Yc!;xM?B)}Z;AAf->^Z6r9Z{pzDuW9#Mr7E!`GdUW_J01 zMeZo7cyM!aSz2{JDMMDfRo=z;sID44A6e!9;1kzNj}lx?EKL)`#EXr_4LEGKlP_rx@6dG zb3A%t^T@ver1~Y%plsTAeLofPm1^E~!tpk7sc2ki_#>Ccl1`=5--9c4cS^__tuvFE z!r@9~j{E_9#gXP!O5_0ouV36{E8SY`! zkrLvmqVah(;h)2pIch9GC%7W0d~bE;lxr1jq#1ER-*CpVW5m6bmOl1~xL4r?fkjWL zTC36AC%ii_82&nZIU|!a)7)+zh14WQ4!B{3T*;(Bdh?R4mU4QZfry` z;j5O2^5ARE5_M0>|J3zqdO-{`$<$sx89zy@gt)V_=9U z-}ppNkJ69)P;McdqZJ*^JXUpkx(J2i@448u$JbWUA_Lt{wRamr_xB-DoDy*4f<9pK zJRqg#=cESXzsGGgi~Y3h?3xFMHD6&Ocaqq8E*#onGJIak)HZKsjSIT^c5bTPQ~%oF z1Nq{+zTiYBCyi^h%_bO1?{Yfcj<8xY701nxtLr8#LUROH2YxwFc%#1^Dja3y6QM~I zRZbNSRL}maR#CnfXor0g`&wHqdYb>X4m682d_7h{-N#(3z82SY7^f&qB$woGuGUGE z59%i-1C_iU`}jlS)#I1>{O}v0g}+vTfHB-gi*?;!x=ic3z5llfiu7)pcXv};e-r{F z|6=wo*B614rN%G}W?cG00wl;8EU1|`t%-cl55@*2kmao)LgR~;kwiUx{E zp@!f^E;$Six*?4B`+<^z&#aM%LUO=Plit*sSNnLt<{n4E8So)v2v4RuX5UdFg66v} z`)JQbjrF3yMf7yTr;|~B7%Ezy)z%yH!BH+bV_RyfI`a$EGI}tIMG5h_7*~#a(^rbM znI>H+U*VUG@icmgC+aPtZs0ln6eWHfsd~hB;^=xCixWVP0OD1<(wV;eGf1zzPF+S| zL44D572Krzw=uVxr%({{?wdNQ!ke^;>BP2K^@6`@ei0`I8HW~WW2pF>e-8~YNb2y% zHG&cG2kna&6)4MO1ZOHAMnPLKVWZ7@Iv{-K$Ncy*qwb^5i;5DL=gEUnYIO&T9<80_ zI62yILdc?1pw&oLVg~Wm>B33q{QH+?al@#jiArlPg(=qjYqnBj0;z%ER1^KjhT;`^ zg@&(HfG!cA=p??!dyW`gW?v&3*xzEoN7)3+{xEfLYlt8G_P~ilyEVxZfZSzTaZO=7e@!*XS z!po8vy7^clykL#{?%5Fqk$5vWT<8P?s5oW@=VD@5p#5a<8#z2a1f-XqwDJz9lhU;MK-53 zY%XxFr6R)ec@|%YcxR;c!94zpsO}ZnB}(WwWAus90a{nbFF#hg1Ns`3?7qL#QdX9- zoaFeWQhF4Ja7N}st%d2Mi|6wNlh=mepUc#y|8-TjljQA(Aefz(2Qk}MgHUSzcO3|)9bBGaV-gCKSv%fc%iH=9#bW-4wp)~jWFWG7z%kME| z7e`n|F8k9dv99I04$z~5eNN^b`?`7EU^HaAMo+c;`PD$21q0r_<4DRVx5*0rj&7#V z$}J^%?^kWZ3938A`-NbcEbE0VNx!!z5u)H;@Pa@mb7Oe)PlMyp5XPkR#!T3fZ8MgM zNr12>TXHRK66^b}3*I`?oLGiAf19{p&2s1ihJ$jkt#!K@RU4a)U^hact$=@6sO{?v zU2VO5rJi@FP^Z*ckD)#)117k7se3Z^n^XBdHp3(i$y%V{HqL{m&U~YneqCcZv3!3%zP(Jn&9{@WBAIXeCi zovVj(k=Uj5Ql`^=qMfTW(@Dq&@rw8&2;4S<-q>0Of0HMZG4D4AJ^#A9luRZ*dqQl#6 zK8B}@wrXIyq%$53v#Kl7QO_gxdZ>zu5tY|uBUo>1WAUy$FOvxviRaJTWVe@SRU$T+ zo_w8KPJS|wyFKAeqm#|*7Z4`fhl2~;e?nFDP*58&Lrl*R-o8`-6m2fMHE)}lW=Rg2 zEC)lw%T+I#EX1?RCGxRms**%kCKADw)C6^`04rhg2}Q$AyqTst|x0&=bB&a zIqV%Qx@rBBuA*9uOZNDUB_u}XW;*7nUdgGlytb+I{9qh?;KeKe;D#Zy$ZueDH_=4? zL_`)`f3&$>Y%%laudxl3&RCIW-gtbeHb-TeJ`OAa%xDxXrR)|OVtT$-Q0}jVgNFDG z&w93(v}RzAPsmA>t2@QrPx3!`2^fphxGB`C-+e{|7mw-6uB$iX%^vYM=<5iGWzlYw ztW=59Bi2&W8)-G;Low82*Hn#B@-|`B< z@s`b(^u^-rBN02a-t0?Z%ExH9ZmqySpm%)a^x_pfOnQAbY~Op>nXxd29*=mspLGTe ztQpgev*c?+0KFkE9z~|npm>RY?Y()G3@HT!-g#Zl)A z((pWMS`p-%(U6wR<3zPbRTA381=??UUC;10=zk4+A$oTRCnDZD$9}~Fk7WvrvrIO>p-46Oy*x3JP>rMTdAS}>$yLOdyCN?(zOlW$V)+?_)kmq@DNo0< zSuW>tN~K_t2t=0sXxzHP(i4T(|HEx5n~Tk!f%g&)>hg%gS#rr2xY7Bu5~ah{)sCNVHmGO;5lXnz_^^Xc^h0x^FL3|WE~=x$!~K}yKak1~&) zDTjzwoq1pFofsm*7nqoyduumMivPhm;rNH_-s$`DwM=I$w8`XTf*-lp9T8^a{jd>3 z9^$%ovv4+WZ75>au0#ETJ>~+_K}MEk4(trp&qx+95sDc+HUy+3H$6@;zItnGT<+(J zu|)2w8r#H^?EyVMld?NjKb)@=Y^srMC}AxSxl^Q0Xs&PuVw^^@gJg8{0mUaHo+Abf z@{`MKKE+$F7DY>`np!*C>(Kd5!6^+7*8d}xTD@8{v4=v!>MbZ0JJDQQU+l?qdtL44 zCo?9Nzu;fYeW}|f4xC{N&;GrNz6NZPaT|UV%a?(nwZny=eKP zK1GNWbLOg4VK~D88KiCrb~{)Kf;9+vuXLib9+IDs#DoGf_DjGO`}ws0sCTYOSoN_M zleX!oAL!c^T5{f(lM~mrAZMO{$@1`engVb_4L;OcWlpIS?XG0~0_EzEm`-LZ$&+Qb z+J$dE7rnHRv9bAOsy6&2MicSIzIug&)r0S!lX^~T&i8gU_J_BdYhCgLZVoxkG4lof zfnY#G8NFm-!IG0R79{6nlN`hU{5#`Kaa^>2?yK#z zHqxuEKU7EMrUyPU+OltQ?3;S|Gr2dnKqq=gV zCCRxCvESTn_70SA_>6_&5AnPw!h3^~9VuWgQAq7LLgfDHvZ$4n9Qeg?7>Rlw7=p`p zJ*}QC91Kyp`@TgGV_N_f2O(0>F?|WZtGz3Wt$<2y;d~nl7t?NsznlR@OBlCgBY2Xs zPh=aH2;dIBtCC)>f`!Oz=^OclCCV^g*cxgNP0#jvmJVc@kG_Lor|%ssY8-3&I=8-` zU_E#(LT?&LsVoqAdw@7AvJQxl1s%@26$-=u@>mZx5<8sXT3c2ibqHr4lKu)nyQZ5M zk6ezLm@Y3bH-B-u8bTs%@mY7Wi{rf}D>Cu+Pp}!)s*37%%O?QRKa&iNKAd~(H***^ zRCmnRPVX0%fkum=4SpCJqVf^b$lNxroznA1-zj9QG zMyxNA!Q96JTGcAUF=AIPfk60Kg4RL(S1quB^WjhLvw7`mNsHxql|{!!3qZL=Br|Yq zq_y6-536GZ>ATARnEn?kGe@O`x2@dmRNyi14-G&^&wzuBx%{go0_8N9ESTBb+u$#U z0{k3P3WRSVj_7_5M^8 zBEl@B#(5F7OnW|qPF8mFLR)}h#q2r_@A4XTE1HQ&rfC8}toK8yM-;=H!i*UL-%?M2 z-AgFCmb$(kxu9>>9V9J{&n7g=4IVtCe5VJmsYf+}+U-=)6(psQ=%>gR zLz4NqYOF9y-F9lH^qb4cn7o?H5q~X;%zeGGg2nlT*)?gl?c>dOhGzOve!>*1mo zjLY##Vyo+s91cLsi@U<~I$gS)Q*m665>=K~zagOP~UaH%m(8`eO|7QIl z9_%N|X0};}=gf@&c00~nBz(_|c}_;_v4Om`f81+v(Gw-?OZ_9)a8~TW7P7ckBjWpN zLW^pLJ@iDv5ib#b6SPB^Pkn_Tq_)hFbI5rzj4>3y0BsPquVzXZ5%1 z4y)V}tlh5n6q#8s{u&KtxP_uNAYdA8wb_q;H=oy}AM2tuTXjPL_u1l-XZD+MCt)0| zdi|at#$_;4Df*vdLWgJ3I8pBF|JYko#TelAKOTwC&*G(;-_Us6nRT1m&x`l&guEi9 z>bj|ZP;5L5*P^GCi%89Qq;r^*c*VVR&hk=sXMTQC z0A^r=;ael@Mt?-2zy?3>XEr!+Tygl`WR{i|hNJCD+Lzsb+LI}jivoUOwHu8T!5c+m+_ZK6G0qdcsN;)NVIhmj{R3FL$(AM+QIE@ED@p9!y9|gNHW4(yQPX;#2)HyVHQk5ZMBKm_ zHB+CH`6NG7(P2>hl9fqeFx(3ak8!OpCjvQB`F-_hUVeD0|nX2kKqc26eX!xvRv{!NPN6~q{VFH zX@}~Lhdn#6sK1wBQ7*T}#BSrRac1SVBk1S$BkcUrH{in{0%;L~^mEa`R{q`U?XK|- zG#)!dmivoIoh&K=1gx5-YQgr>8!aSkD&0y#QK2nhZ(8!*4 ziToy%a~*h4SFURL{3Ngyv9iGCvb_A>W38xV(QQr8rokGo$t!3WdfIJm$!?vcjoJ!@Kb%F-XJLhvQh^#q2(A6mGO>(aUGukw@r zPRs1d8lZ#J_d_&rH(sKn(QAu%rvXaZ@HAt5zIIO61h>4nXUBh8@X<~c`^~r9_m#YU);t00QV7Z!8GOT zIg$-eY2Go=-vy*v4Iy^pi!4l~Sh}CjpMN2O?$FS2J)evbD*#hR9;E9zG8CPXbxw~b zHJC`)^22P2YR;VP%GVaLJN-IC_~dHyJ~Jefj&hR-xc6`%EEsoR9V`&xlioaq&;FjS z?m5{m-Acp8gAmQegKho>|D34{Ba-~Gh*8sH1-t&U&WD|?HAAO-R+*%?d{#jzw?h$g zK0!FV*&khU%=ycT&ek7B#CF@h3B{G%|NLu2Z2oe|$oSoSeT2%tO1Mpf#G~r{IQH9> z$a+oZvS0Um;Z!ciyWc?Nk#e@D{HVy|bGbiMDt2c-%Lo6w-6<7(_6{`vuc{u}-xgkg zP4o3ipe8BxX1Ylm73}rEMT11I@I6~N(<9&xzlZQ!eW&ZTnjWEab{jDwq?%#FL%q|? z*;?C@U`aWQH6g@gqGrJaeC(UEd6wfp%VG$e@db}HzcWlc_EU(^&Lqn_BP zD}KSmh;bQ0`}D8v+cGA9qz^(X!#%nPP$B?vMFFPG?ks@R`uDap5X$lYvHdid{oDP? zRLj}YYWRgu1`WARXF?7opVb$E1Q;BQBIA%zKlN+Z>k+4ofYxVOC(Le1SO_*oV7MX| zg5qER1vVJQ6?^eTOSPIL>;lkyI1&HJlJ~uuYLM-={^qu(f9o7-ZU6R1?W z_;VJ?0>hhr(EnNkpl_P*U#rVnrPXAZuEkJgoy34XD}S5%KwZl zCw>QLD2N<*yFTv_KOQqOX7Kp%RI4^VE9YIyyt7laSkC0K8CN1abgueFKC>&27QSGpzn zlle5S@!~suOs%Ofw+O55qjW`j=+5FoPL)neQ3oNhGYlIhAm{$JhpDY~*}24R^})yU z%Do@`(INY6(-(^VS*57L{nR=8l}$aq-u=~L+O)=Lb!Vn@<}**i3dzHzj`c#kUWLdC zTBx2%p)Mc++(5siFg)#Q8_Bos^>`9Z zzmCH|sEhgEOBcj>_PwG`EAer8=jI81EJ$F+DFeMz1>Qyr2f;A1KS$^X%ylI9s#o$S zWx%~_u`}#|NNyEf37=&8c%=XKecA1J$`m_5p2PCm9W^d|Dq$67-^Tj5#H#sJKt3>Z z5FxCN!=!V&0w*#ko*$Ft-=E*O%ZmtT9{lo!<%HqBZi&f_f|YvZi;M~UCWkZWGc~#N zf=!=%#8P-}QGf3!ZEM`YPz#U|vpn9C7a&|S0T()H+&4gp;zuLqiXTFFh^nRfu<&;_ zOs#6ITi$W_H<#e08$Jo9 zy0la+J)x1uCBzDUJ?6BZPEid`h?)>tskhVjE8GnM$2lS#2uMYkoXMl!O{@1r3l!{np4i4{QdD;4aF&|@<)JvmO9K}_SM;RaA+v}a*Z-5Yex%vEUTC7o*J=i_tQL7yUm3;xooT@WmhRK8T>q8<&sV(lyda)k8(l=EvX zPU`e7w*yhB#Ky;qWkyR!-ZB88kWpP%x;i}mZ+($a$;={eb8Alb~_DF5G2a$WbAj2TG+K{S=OfT5t(d95O7fQG-0B5_r*85ZWCOZlYLwFQBRT2?ixjU ziDq5~$%b`!?f$KWqNr<<~83cdU?>J5nFQ0_!CH0DDo$mGJDe*q2T1#iz#G!l?C zo-U*8r*a;5YF#J8Cl)UI{ck+pM7ImPSRhJH*Q~jPrDi)ndEz$1U6Kpr^!Sg7bVfoI~q+A3p9v0Q$e%6lU~22{!1obLhYUEOw=vf0Z>b!SmP zP^jZyxPm7#5nm2q8c)|49ccu`@P}r%-4=!uAWk(saTVr@53%tf<>SHNUn9HiDh#XCu{np?l0rcfL*NgFvPo*kY z^0krt@QSy7Z|bptlrsz07>+3}qA>&j8$M`j3(CiN%RkF;ie$KjmH0`~o3V_+Ec#!_ zWeTed*9)&Sw;W`#H%%H1M!J{{r1Qon!yh~+G+l`3tL}Pu49Ig>l@`VO;VPS+dk(BW z-BJ!Fh|oyI6|abGgM#w5?tEX6>`@PGG#5c#Y`;WUoTvON%Jo+DMt5NlF{*UCTAOQ7$KGgc4T@Kheo?EdLR#R}8j#%pkkQ$qK?RCB%u25>S=C`DtaD~7@W1@SJaR|H!(fi z+~*=%d%LKHlsGkaGOf?}&-+l1D6Y477+Yn4YIrA#e98lbKWMd|BtP=j*2R5-~DNnR6#mKK%}KR1Oe%m4hiWTx{>Y% zsiC{OQ)yx7hM|WZy5nr$?|Xjhtlv5R=~}LFX7Bwx`@Zkzx;|HG-CpZ2*=->1FINH^ zQF+%Fz+nR;=s$*OPlCDqS3iSfixcMaVqc17{v=C9*w{!Y?zA#?dFb&@^fZ$H$fW>G z_`gJ4Df`2AG1+Znf4S;-@@Tia@m*J31SMB7XyioRcl!qjvP>IF1QtGZhQNa`T3*5s zoWtJc8P@&!y~puG6+}e}F=*Dr+U*x2V9}KeJBkmCoVUVXW_&3aQiEtP z$?&8Up;99^nE97E=o;3`%mNEW_7j5!alBuaCD zuxe%J5^=>4;%}TmUMSyIDTaQQYa?(yk&{={vWzji;8+uKP-GJl{rc^c_QhOU-X%gS z72_XQXc8g+XAv~IY}s<%Ch78Z^GcDjI(<;qsb<_Azv-~`an>fAkovR{cw07T(Lsz}K>`+N3DYEA{^B6G-BtT9QO#Q~;uKf0AR-XCh4pRa`Ys=a# z9~z`+0m}mKY$y8w(``5RZh(ZAI!O;ji-1Yqx zDA8DK;U~Ws?*4}{Bn%>#{b`FF0Z(Ondk%h&i{`P#kamqLD_XI3z58fgk3X7)n5G?- z1oyi#Id&Et+4w$a?o$AEpUO_x0+Z}hpjY?mBm+wr3Fq0=u-M>cWMsw9IZKsm@T1$< zrw?D+e?G5u4`yE;cNp1~F@o!`5570qS($K}aGBIE=3ql2&WP=c0|loQ^fA5;LKe!%0< z5<*!vU4U(r^~H&q%y1F20aW&?V@W&c1_Orq0fHN1r6dkC9eQ~ z`c(y`o82O)F`%xKk+VjcV;V?BvBIlG@oZMNp<9es7qszA|AWJX?hum-B2-R=4=Y0v zfA5}5a2)<=GvCw3uqBYz36h&~b%_796O)kH)J%uhWRnTGn;ge= zO+1liZ@D0a!mV?`=tLKD5OWn9-)yi@eiLh->qh_#?LDL;${!}hwZ=DAY~QKfMwL*P zM;q#miKEJ^@w3XIQoGRu`M^UuGol-GqN}`kMT!Nc!ytBfyh5&l9U3i(tH8jAqW%t% zL|p!LoPd5_=HLY|EY@oInbyde+DP7B$HtKkbk6_Odj;R~RQCEir21wnH_X|LrrlQ0 z+3Q?Vel>^!gr@(t%paD3q(k2iI3@q>o8^cc4LXgC;~E;sxh!Vm-zp*+d91V(Em|(Nv6~oM?#yPw^Xj4imb(!X8iFFgXz=U3NXy!EH; zh3(g>6#~h8kT`87*u$|6fsEh@w$1>AJ1QB-D77CyjdM=nXm7Ff)DNyzypmn26C6qSsw+WKtW%2TiL;XIJ@Uv7n> zvO){hT4bErY<(4ABxIBExI3TJxG-&Gef(S3Zm(9*7eoW=UBgc?EM6g}2eR#w)2~Vx zXZ_eBeIckKOL8J(9R*VPbYD@1^xC0ZG229 zgcB_w{n|qF4CSsyhD*DJTaS*J2FqXHIY$d+11^YK`w=i#4MR5!()Itc2ex&iI&$f;E;9pZcpq<*ftx`Hj9cL`;l zc;R+a(Ld9J;}hx_nRT1x(xZv|*xiU4h^X^)Kv`pBt7>_Y$hEblBZ9beu|QCQs+vdP5>Mm0grT}b(F>r2`w)Nnjj#mp9;;eUU(}wdZqFN`l z9UTJh_uQLlhiiC#g$Q~0pRnG!q;rQ&7b_cwM=0VytcsOOxt3aPGu?=U`L_?1aQvy8 z{t*%ja4wYX>pqeQIG_bsdEen|s73Eu9dKj29q;c*DT}%M0XpyUS3W+|QRt?&xdJ@l7q;zp+)BCRNO-F=w#aHp;<8}F$VdTZ?)<{Ju6k?K(;y({{=9|Le$z+kV19}&&@%i9=;$@8n5pxMH*l}Sf}Phc7NaXa8wqEWXuyTB z%1aJD0?`9mNXY52RR*s{FcOXD+Z2(1bh|+-ba?JgrF4?55ZJAc$c8H72pjC6{y;8L z3B3c=jW}EBLG&~CId-LgBn7t@Rsb*6cV>yKq#(J5f}OYu-VM;oT@;p@p&WvqGWr{K zJZJ1o)UId}AxSz3>TUql_s5JS3I)=%-C|z~)x6M>0y>*cc6RQFj^Anbz2`YT7Mc7k zi7m8COq*r&z$vy^Xe_qXYPeVr>{4if9-W=5S{enLxKdiGgLJJWDbLML_^<2ND(xb0 zR4iYQm_o+Hkdc2D?wS7hjr7#+f!%OuXPP;=16E2ng4>fCy5N9U0PM7rQT^`oV6|b5 zOY51SKl&+XeC;(>EJEbtIy$4ve2<6xSGhNixkB)|^^H^lX6@VrOZ7}k_lev-e42jr z0n|r#6cGTdMCdsU;Hc`AW6ckEOybqaJ!?VsrsE%2^f@ZIUy>Ia7ug;r7ObA12y|D@ z&pI56=H~AYz%&g2gOmy5jQ~GC_{|&s6XBpNn?8567+lba^+?K1C;{5R*^R@Mn2p&WtbO5Z6*zQe-}b%J^HAmKk@qAy66} zbgu19;8SlAlg&&vSr_edm2)cC&cT&nwjI3NOp%x9ARyuJKs?Fk)xDn^YjM;5`b=c$ zOtE}GaMa}SI)cYynDgG@z5-i~4YAVi)f@z=HBZ0qfY>#sJX3-T;@}&J43$_k`clrS z*YX({_v%MD@AFSxzqJg#oXvQKvXr3vnr*jYq0Nt{kO)d;F1V2MbNI!0B8if48%6F8 z_mHs^|* zzyI#toEJ-mj=a+|Gnz{mvuZMskV@}Fx@r2Ze$CzA;uXf2v%SjC4DNcCwdL;zhvHlZ zGlU1#Lgf63s@C$=V~kFz0Bb0Iq-OWLq%dRRhYQV1jA+A!qxE&#tNEP`&!QWWasl2( z;7t%F65tbn#)m{aTj6P!Y$wwN5P{#c`f{bQGmmC>mMs%*%eA+61W~-En$`8Gv8wLx zX#ZK_kr~VcXoI=Mq*A%8Iu*VV9j+J;L=#%gNMs;t*DP1)7GOw0sJ#Vcn4R_}#mnO; zhs1Zy?J)uc1F`x$c}&dNN6bxi?L|rX966Ji4Wb%D8_Yfi-3_T01wEX=PP3QQ%rzrp zQI9!I_XR6DU3dPvv&F<6a)x*sDK@&==l}g%A#qt2geCbB*GeZUvfoRr(#VG@?{!I~ zu&gEqn3_Q#SAxv>klnB6oa*<$OL8vY58_gg>h`Y`3B#?={i6$BosM>7gZ!y&<1;xR zz%eXUUeutiRcj2V0wjjwQGu0j8J#@7ROu`&?tRgYAxY^aE?<-T4aK{M0gA*vvifX9 zamf>ppk-n%?jY*vfV9RnE-lDx5bUL|Y^HFC=`!HP3<7V6>NHt(gDT@#`eOzSdF4b` z4Y4<1-91Aa53s?#rJ84*84H@5-VhF%;%k>>-@`z9CfRp&7yj0}WI&YE#{GK|N`>sp zKw&`(nY3seM8g@@?3cGUJlm}^M4l_Hjhr6K_?f~p@#;YQDP=>4tvALY&FC@t4 zH7ax|voV+KA@gdT!_lil!P3KR0aUnQRO73=V5!Y_r2tv&_KpXDg2<>-;bCWNm8pXY zD_8Dz4{eCk8|}AvGrX7)AxUyF_Kqv9%x%+GF95?V-*3tOKgwPH;^5!xi1VpAl6up6xa|1OYMOHRR~$^6Bh+9-xS3 zg5c+sw(D9dcvNHZDJi$M4YX5Lw<$hPNr{ylja7UT86o<`Ev;+{$m!F2L;T-waBg)P z4Rx1zTk)KG+4|*9d8Vl}R9b$%`W$Aa*=YTi@&Yk%uP9mFlK#E=cK?OE^{#4+#I5^d zKQdUoP6JeqHJjuNTlKob5l@8OmxhP5lw!MM)XV!-!=Ra87 zZ2uA=|K@t%qfs%kwpbk6+O0^GK4<%P>+2^kLQy`;sM5So>k?hxRg<|<^WrN{$0tw#y&8|$hRoH4dJAPGx^_!*}50w*sMc@Bu_ z-dy-ub%KZ8{z3`Wkei*T+Z-!^wHv=aPb%iLO+!Ez0YL(z4=55R?=JVq=!2a0)})p( zy>qf2?{W8uLJ^~ptdVf({a(4fu|lWs`_B2hZY$K&D6*Ng+laTUn_j%DuU;YlHbP_X z3pXM!GL#wbk?M|@4)!r%Z8(E|jikTT?NsF{tRcW5Clawg8rJj6p80(xz}%BcVKLZg z>02D&SDF(H@#?bA*AOe!OaO{W9Q$R!)f^1(CF*Fme~@x?bFzqDXQk{ebpz6$5- z5$M$IjqhEeTzjlGHL-F0g6WIC0((`aV{@gN>+@#@UnNzPgT*+vU3U zXh`{o!ufobnY^1=3op^r_S><>{E2X!jp(xv5##&*b^QFp3Uh?68W;C2Xsvt^>jPzb zmBQ)RHQ_x|e~@!2+Q3^Me_g)`0|#qHX(>csBq%0^Ri2EH1m3%$n`-E^m)phP`7lJG&2F z&d~?^dUfh8<66nJP}a_@`IPbCSM<#UwW4&>d^tee)ogOW6j-OHkF#6${@Cvwi|A|$ z?Kc~juXUwd``D=kB0O&uc9rIFRGh#BAferJ)=+0rvkrzV>Ud}G{%<}60amv;A4mvA z$;i+V`i%wA9Tmp=|1weV^rICC?egpr)}o)Gcwnz0nqc(jWT{ZVO&7~=ne62J$#x{c z#?Lzq>MsoU@N%WryZyb9_GY3^p-+z?yXTNDLn%&iex})^_iZU*-bB~!3{Des3=2M= z%aO~ycV|=iYfU2NL9YAd)Q4=b0Zq0m>@DE9zTuUX0HB@x(;7|n6RXE3<^m2xLvLMH zbbiY$B)@;1J744bULY!78+`qtCQFPUw`w#zlAxAwGf-ExC}%)FV(?OuLfA=j_eVdw zjHpyM1tvL~g2lL9qBX_-A>}W~Hsd!3i;sO;Qb!Lc3*f%Z&9+AwyOf$NX9U!zm^<=0nCT@<;0P_clsVRbLEJV1m5IF(6gdQo`7_a8N z1&H9(!w*X(VE8_UtSGW#>6RkxvWk8wJ5s{IttYKV`*&xj!bOO14P?`9@XU`5(Qklg zZMpDplSo8maPyuA1A!Uf9Z zRb=9vjHn){W#*d&n-iWBN;m0s-xDD+o|+%v@CKAj;?_=WmTVK;3qsyP&DtghV$K>I zd)D0w_#45H2MIL9lj5@{B`XSc;&W^mZbJgi>daV2h(#PP<9pAZ(graIzo(XGFe>Fw zjcNn`rsQ(Xk$d!2%vH#kcSyrRc%t_!uHo0GVEP-e^`rM8ky0BzA#O1pJ~00YA%ts@ zL{J)1KyR_^^=dmy-ozXksY`^pQ$T*hU`bO%!&VKD z+QMSv{exWp+ohTPLlBw;sK4?#mHk`30JbejPj=qZ9cxtkgqEVmJN?f&#;+`$39};N zIIT6=Nvmf>t7sj+oOdAAZ4usxIaIQ(YB&i0xtS|p`q-TbBi2?WYG#ETw>g_wX-IE%%1`qPF zVk2!hPvwAi?bWEBKxkTpe#eK=Uwk9QswGt9MKtK7eG79c$8=OWW;17bZed|mJaZCUTSmE$X>W>=~?M^z)0d>mWMz&+6U4+qd(C~MMf%!DB+HE&BCqvM7IQ}U8L{87$n>|>5vThYf1r-w-Ts&7H7lA7iBk4PZIg=!~YK{W#cuT`#&<}KaAu= ztnnDv4n;R~R@3H1BqOtu;qm&D&47u5$fx$B<(7~KWa?sTJsG?+ zJME3H>gvx|+c0mI#!9k5&GpO1{(&_-2Kt82JQ6kz0Q?EK%!%Z)wir?Q z)JB8ODtiS2`7_Jjvuh-GM!*=bPo9PQuNn~$uRef26L<@=^v?}HtKZv8d)BmgHtg87 z5bV7vn4tcX%KEsQcc^pt<7xycf=o+8@z~ zuK|+haUJ)(iCkV=T%Jhpb$>w{r-V&y7FF>E-M1zC<2IuNa>~hgq*z7ps>mTpW+_a4 zLGQd-FukdI8=A1`;Tl-}vE>%B#ee^T*$o<86o6-;e9EnA5`mXA&qmG46kwMM|T^D;^cL zq<_t#G-c5QBG;y&n<&YEVgcfoN8)v)>XY;)5O+-ZfeMKn%_P|%##h??EYW{7rHaO$ zW$*FzSRQZQ`-9%1dIMi}Y>zOvCbt0<*0>AMVU~MV_S16?53i7@oVW{+^CYIKElR1X(ba#qbh?r}MzlW75hDU*|euX%!%yw8DJ zFdm2T_kmOz2zS=_po$@rlc)1O2KWH%9WTj(=H$o>SqvH8vRL9)(a-cd{bZ@M+kx92 z*)`XyhCR2ZHju*Pj4au8rT^|5pRnVTVj3esbC zr<3lT?)H&w1XSlQd_^j*+X{v7p}wN9N|9VCch=n*DU4+L*H>`bpXxv3088Hg$Z7Vd zX?K-?CA@c>C)oP9_Hv~)i&2Muk(|x-no^{oh*e)2LGS-@edwg%pMv*vnL23SoE2CWN#I7oEl?TUW{*hntVMB%mGNM zx#4?^!!2$hl`BJez|Z8fY_C76F-I7mfIcppEkC2YbkqVe)$&bFaAHB<7XK zVxnw%5e)d95`GU6+VODVK&1*AwQMm@eazuBc< zgj@D2bsLL6--N+aNq*PCs=_PXZ^Horir8DmZ@GX5Ya9RtIDwUc#^V0ddkFBp`uB~4 zU-FN44BWms^uav>tdhfct%8&!G-c9ve_ojJbMjgfe$AAP&&M4eEYNBr@r?fe?m~Wy z>|6qjLt2Ees)O?6p}-x8fuV!690_~G-Q|~Ovw^1e&Z5w@vmd;_fRR5z>)jbIk9a~C z>70vzaP7&fOizouzkG?{xRN7|=RFSXS}*2HO5Vg)7r1QRfI>ci_mRcZJ39A>m4ynY$s{J@RSy7-9^y8*ts`&jeW!XoSUGW>D*oYq}$ zlC{Q#2J+Ynoob86ZuRa!e#*g3@ckQia5p!|rSc^_p)LbYT4X@`v9ITx)D>j0cxLTr zo+KCcduOsFf@kk((+n-A>J;E|ye@SWDr|pv!1d_-lp+%c^qMb<>l+@2k@L(82il0{#Smd!6ns zw0xdvFK@Z6r1@FC0-6xk-G}X^F1f|4kQRt}Pro<*EQkTMky1)O^MmqPh7;N}cEFM4 z$@1AhRLRqq&!0y>Hae5GI^ALf>@~rc3lIb1OXOMrH6(46Y9^(yW8$!vhJU|eMsJoe z9y-xVy9A9ylZ@e~gZ|N_+^jF*;bBEnW@8TccOCb*ykaY*jY3DObum`a2Qlk_I&o_g z?`xP%!Y+Wh5DW%bB&sR4$h&n_rOJ!ts94~vF8kDdttFKu7xLY^%k+)bBH);u%aVL9 zn2c;?@3;fq>9JoP<0WQ+WW<|*kZ2`9UfmbvO^m#m4&r?CB0EGD2z!-fp46mq}QhXw0SkvJVXw`X*YMPZF_#Q8&-txce z_XQwxz|K=SbV|zM18sHyo_ry?F(lZ{LZZvvUoCC~1f@@$odDT#64=Px6;^k~uVpe= z$I&CXf}@@xduQ8O=I~BrqcMfrIQ>-0$miZ5S;24On$!=XA^70In!!-0UEiyL7Hn2{ z0U^Abl}Ny6hk!&Win!E}^G~Z9GOxS4`(HaV2w3A3s`W&EfN9*uI<^%aM*wj6n(hr7DtpBvn; ze9vYX6=I8ljb+YtQk&XV>UR`o3h1H>3;^S(-R!l#fDoK>fqJ#%JqYO`jVpY0)3d|v zH22pi_*Od-(Gh(PNc1cIvfwS*t{N%7j_n66y2&itigts!UAsXoTcY45rtthqy*2=a zP@y@%W|YOSB8FN%e)kov0Z-7N4d96j@Ec<)mnEg|)#L;S=KIlf^LfHgX3MptfOHkZTW;G2EC&T%_@YJZ+vS|d37_})#6HjrL!8g zuHSsz-6Jk&HcxtaiS(E4N`3c_8=)eWU;56Tww+tdpR^U7;t)fYi-DknYQ+t~YF9X- z59Ah&EF#f82ur++$V~d(zdm1UHR(#|c)lhDAg1))nJs3}*0UK~v4yCBoP!xPD#zD) z#NAjmJH1eu4QhIgKI!ja-mUxf81Lr(Qr%q#rbjTg(o4L*dA*q0sSIwU=U#B;?Jhn~ zmRa`Igs1r!%np2Z@_WOSN+FBoz@bB;0^cW69v>uw0HHe1s?QnDH+uffZXv%gGfb!N ziX-XKEt(Ci0VM5 zXW&o1>Xx3xQgk)d{gi#NI}t!eMi#-v<4Wo1=rTjnIPJY5Dgr=VYa5)iqewYqOs%{5 zwsU#K%e6bA(|GKpwX_zrH^t+=Z`L$JI03sCmHT~Ad_^X?^TBK;HbeO7)Iv+D8(rT3 ze3MYj*7n$X>sIpxgmkHunb!HRNAjDuA^Lez8h?_m9;uR{(hD=H^OKWoAOQWKotlV8 zqe7d}t>*PDs${Hzs4C!nfS{>yJ%98&02XSAdYNY0Cx=>`;*u}lf!A}Om-pk~>T2B2 z^gvZ_LJf+LzSl7ui)0RxSTA4ZdCW%9(=e%&D;uQ60S9zquS7+~ANJRj3{LWdM$@|n znGh7Lw%VQ3`JMM@Z&nhPg}iTxJ+6tngRL4~V=7QI*+C0|lY5miZoOi&swcq@+aLs3 zR7d+zPmOTQz-kWSfHE;RD(K*uiR2>Lkv>xQU|)eh4&~yKXJ`v1wNwu}kKO9@9y$aWytm}) z_bcF+3g34NzewF4kqV3X>h%ZfC&cg85fi?wYwG2`CnnUmB0gh#Q)Z4d#4ldA4^a=q zW@h45QC@;>Zf`l19FLb#cht)ol3}9p3JY1wCZIb`GTV6gGMg76--hUlOptYV3g0ZSt z>!NEn3G3l5x5=~B90g(N{>irR@)Da!>nCyjsJj_$1h1_t!WI(-Hvb6UxnX`OMTaQa z9BbG7Liu#I@#qc?fDX%LkIV3|EPwdj)+oyL$K!HhbXtuC=ED5q%Z|<9u}%~|biXo# z=N$aENrPhs$zx&hd^A%|q#7H4W0SXh($bCyMN55o52N+g_ayg-a`+_GS=`F5#O zgeseH7i7L(>#&1DOyiQEww3;%&fZ5QOS*e0c5Z^LrX3v(jfc+F6<1%;at%wN_9GNS zYc*dBpUeKdphrCTEJG*W1{D-8_fbBLlh(?L(voCSY?q3~NGN9GR{5OFPx?>`u(gKP za1n00<^fR#wZ99}in4vdg6`oz{$^0GDsu+J7v?Ul*Do{CByKF#GZy4+lcouSC~i@{ z)ER$|*rg#6`Va)0jPLl`!c9jPQeIwu(-I#aU+j4eUhJ4uIUySZsaNP~83g%E6V0l=hL^3BjYeGri_1zmBs*T4GtH{dxC02yz?bxkb&?itZxnuc z-(3)Tmg;jIPQLdn;!nQyJDP7qDj=WFnaO-DXDJW3x4)u4-i6B9Z3xQcqh zDPD`(eSzkHW=G`|T(1R!TCy_jEz~x_@y{4TZ+|Ct7A+&YVC|{`9w6@K=nHbx$kT%* z6X*^8`?Hy~+tIcd`e&HjQgq@`%Z42t%W}TU?;T~)5QlnUfskV%kKrt0g{7zV=`}w5 zNj?xv71`nAs_R-cL1HhjBA9;weF+gc=-9 z`~(2Mvce7^lfVmlaBNk}_~B#ZC-isEN=#dUjhQ8S1Hv2E1KRewcoKKvg$n6ZC=ER1 z{Ep@3)<#%TjtZe&Jb0KqNdtg}#_?{{&PUvISZ%|T(fH;r-EJ3fP_fD=Lh!FWd)|50 z0Wx)^Q>mwR<05c9vlY6hm|?UdfHmloO{x1d!;AgZP=$Lz_@@5ozv6cwstXu)zI@~` zsM;=h0v92F+H}o?3;tz96D4eutQYv9UxaoJEo6PDd@z7Yi*~UZXE&cc3gDhISGre{ z#TptSo0H%#XUtaUDC#sF8E$Iu>q-bsMfe4Zv~Ur%CO7d6PI; zw9#?}hbmglmQOSSscMt8rXxcQ{&ZYd7$1LdBK{+hGHAFILLZJn(Z$sle&M_Rp93T# z>O_S~)kn1dj)<6F?$7WC%SPHv#BZxT-G>_;OHN!JwOOdhFf0i0DnmOT=d+Q1|88|o z?d$JyJ2kqSBUA?*T+9M+AGtMmIqUb%hm*2&XXv2dTkTpC`k68_d0^yVB0eyjRJL$q z%6xOUKXb6?13>zy$IRbaoUDD{XkDrI@#eq1xj85C`z#GKn;FS0deTwL9lHlrzS)qj z@~<6tSkp*X7$R#PL;*sA-hBePBM(b8sv5PW2S&>HuPPC<&{0s7OiU8ps?v!~fcpuguoi)hd6!HS-py3GVmz+kFZ^uV)hykF;Q}&~8Xe3#xyoqoYx? z3VR9GrR#4A&ccnR92I+CES>NQd`&KJJzl*&lnZsNR~w`aG8Dvm5lw?$)c^3W5TM;s zuhnFA{&UU!3-udA+{40xK#4YBcdU9Ab)bMsq;uPRH*Qh&R^&D$G{tRJdiY0PoHDQiVCx43H>^2_xvth}P?qYigx|h=^z#*P z+Wn%Sa2a%{S?G;ZA1R;iR`ES@!ARb?!sd-{K+QuNoM9aBwcoh+!0$v0CA{97+?>6& zT2M9)S>y5U?j|$-_+H&=rc_?Q^_Zb!?CK7?OT%PdbSG%-!|Uk^Ps)I`mX=Kh>8D*h zYa2iUp1Z?r#G5;&Yd^2qg@6bYW@4i{$vA?P>`q_Uirg;Us#o6?RTIJ&J5ow9eURaa zoGhwn_Nt(W4zsP|Q^3t%vCX%M_R!?32%l`OfUl`7I;47Lo~hxG|fVSm)+P>+Po7F{th*mKarOK|Besm=Wqp$gRjnGx#DbXUj*T zzmalVuXUOIuwC~)Daxv3ub$}?*Kpw%0~kl0v3pM$Hkb9o-__*phpo5B$JnKZ+yb*= z^=xgU0+;D!XDSXqzWbB`S0)KTGVwmUj>&f1W~dc@zV3R?1iJPguLLOtO?YYPV!oqK z7Ur#+>7<@m;z~P#pWXga!F60x^lk1#wQ?9*fYXi zY5`Tvd!olDsWZ6Wx$6gqXU}+naaW9_dMM{C8}X#i_>5{Nk5`cBUg>dnnKQl|UGA*SAgBeU6v$Y>lV(eT5Cx|D|iolq_{|KY@{Aq4`uKr)ROO1Rx!B%g`Rrkv@pdLHW^ zVrKX0`Rm4k90)ckPYqV^sA$K)uUbzNQyaos>!t};<5-^EBj*xD`lv@%68EvS+o_a# z_>C#X0a)g>(1$5-9$_RB%ntCC1khRr@nA;aN4R8tNUQtAE(qI|cC^9&iX0LRdzs45 z9L4aUB>V6f$ZHNJ5ZkVQ^|z?6=ZuVph>zR)FFty)Qcu1oNF zvC|G~ALy?{#g-u*O(3423KcYHvCN#0B;RrAF56k_p_Ws2cH{t<*#J!27dj?S3inRMm z>EkY5e#r0EHV+0S#@MxyL(UD7nTk9vkBDh_gnxB^!dq;lo+&Ytk_gu*1Wqh8okXXk zY(wF{7aGhU_)&Cs$kFV@-^$oX;ZFU>OPo&g=HcehRK&5sKlltT;YGFSPDl(fOZ&d^`{!U5zQo z9u0q6R&41^TDX&RX&mk0?yhQLLdVTQ+-3E$yXV(zc1{OHY(0t~`d7f;{g8r&B{#*P zfDq{fy2g7XE9J4LTEr}Wotf8Krz&PrWQ$d~((b&J&0381rUFh37IL;(R)>_@Za zftyo2qHbO4)sREh)z?+R>JIeBC^k3-A7i!hW5-b2)J zo->YJd~|dv2Axg@DNnP=2xfo|XbQk!)AZ^5` z8f*m2fqkNN*Ve-pA^g1zMX#DU?s`9x4d*t#mA&7Wti7A9&<887DyGcF?)bKpJN=UZLhEAD29Az!1#I}SqJ<3+9Q`!%6Ny9fjdtG%$A3^6JAur zGnHa_u-;y}0V-eDv*v!2pZFjv#lsu!A5VcrsHKdsc~8iqZ*s!K&)=V|z}Kim zc!jYW;dw2`@%i0HG#sa0j&YQMaG{VGy8|+R*st^JFVDHcHg`nKODKy$NNIbMKe|C3 z{u0qkuMZVO6A1k75%PD9FR%x^)O1UL z+p~5SWI^~$8?JzzrmBYI#T6ohS}lcY1ofkoweFh(6CpR2D|s%<1*M_MnLoeChVBe! z4_huSU1>{vOOVWvcs; z$MCh>agya5!IZf*IwxAETKW^+=nBuhl4vDJCR6V1_N6^arFt1RoTL6ah$Cj%_t&DT zw#RB;#uHGnjlygeAiIRTs=W(eqphqiFrB>R#XKz7}Q${;ORLK zY5`|cQ4$5LczK&pBHSlLs=pVA14*ry`*j@ojY7qghZ^+skMmST_UrG-3T$nUW-@W3 zF@!i0F25=uX!90Y_JGbkchDcAw>3GFFpxHnZSPh|vy82E@ek@xI}tX~9t-s%*uXUg z65W-yF~y|tNv+SF#?ujeh8d00{;(D@cOO^}o_5#fi-q7q70ol6MbK7;*4hJb77wd; zg(6x`&nJ8Q=`WHB5?Swaxo9rAP=A`_G?^!II5Gwk`Q9&e5vk4DoPf_8x+r;Nx+n#E zFR5HxV(eZqB26PIkpy6Ldl{3`6)OAo+IaPX)%e4&%nonS;Nou8hS}?$MAJY$pVQmN zFriuVX=c9uk%sjE3zWJVC1!OSS3LEo)w< z;D@u^=m9;_wA2ttUTL#%gAV-l<$I=FZ5OhC<=*!~1Z$3k`Jd+4MeNbTDpo7whvG_Q z@`u#(z8%)Hir9zkuHd<6IPwU{|DjVd^E4yOS2px6Rhx_TeqK7;Kn*UAFa6H$j@TW- zQ43T)`;(^DX8{W+LW!*P6rX2RqRMnOFc&_9YTPc1rsYg0ai1|EI+kTizl*c2c(Kf2 ziu1vhR;wcdGKQ7I@tQ9P|I@c_kU%h;*~?Rqd@oZw0`_xa{&2Cjf{u2(Pt8oQPZ=D1 zuv;26h@A3=m^EuJkx@?w8#?J&dUMRn^C>U@JBgK{n{Xql1hO`<$egNKA&Syq774*& zXIJE9o9|c0PuC`Y0={f1nL1J*Wf0zAPLu_94+yjV#G0SI>Mdo~@`v%vbyA`qC|y+? zu_zxw9`*~-%QsX(eKS4Yep|mtg+Z|~O^2{9Nvoa#mE}a9+poDoC*N|VFbdeYk)do- zsCe`u>=t7?VAg}c4Q%x-YDv9fLHp;Bbe>wq=3YMXGYZdGZc=ut@d*7 zt7p4pzjLAaXv!r|f~{nyyEOSX!uD-`coBSZoUcMJwnQzRYuNYCWBjMAG9NI&bc$&? z@0lP2E8PZ}xN2=EdvzUIZ`kP8jC0$c(7~?pJPqAdGOJj}3yolc|C(oM(OPMWld;Hn z#YTG;Ih^-oTF=iS+zBv9_*D=5YUPn-1xodB>8f_O3q$`D9PYb&JcZl}8TZIk*VUO| zBVYTyJyqe$FL8blMQ!+k|D)Pg#D@SA;hLB$UlQ0(qGxP910*4x+O_n{fn4913NjN7 zYxnL`)+@e-tNcp>X4Wk9l<#VI=l@)9jy$eL8`jv49fPCvdbCNPr*elaAJ$mVvkd7# zf>qP@1yl|Ohysc0fg{urYocl_MAUg~BxB5#OoR7yOZSHrOYV_@Z5=#F?kBsuW45uE z8t;$!=rR6;+%RfY#SRd7pPWfFV@jEAxam7%mlUN12LNnWwe$3bnJfiq*xg0Kq z-BwCa;4BDp#=9&98~du4dw=`y6?XSZpPrjDkAt*D>Z_!v?f;H%>lvz;>Y8I?p-E^D8e$Lm#9119R6saSh z(066ru!OZ+XiS#nux-0~7&eJMg*6G`L(Xw#?5C#<6^C6V`+LG#w_opkWJ))WHbsTn zW~DL=a#O^(DaGE4P%pmrCf*MY(Y0A2pY`u1_F&kR)V-s`uYB z;Z}xF*+Sosx61!jx%tCtuKf}bp{ZP80_&4Id9e5LUFaqm-j>G)m(=VwuQ3M6N@4L; zlNN%;=rIt~+UaMtt3C2~fr<%9)Nd|i4u|M$ap_o{PZWY)zvUAH1gmVg)3$+1ZW}Ilva z*9~-zYJ?50wBk!jOCP4YWxWP0NyG$OkUV>BwEh~$z*H>v45mDa-flEI-^?G0LkqJ# zexQ6LRP<3CTg6)y`n3?z;Sm4lTD%(Hh^glRBa5$a3m%^AEgfY977zB;3XH>-@8(w) z4EYR&?pv+G7liecvi28RXTO^9sTtK0^a4!Lc%x%@}-oZ=aEv1ZFz+McA2 zZu`w;=Y7 zWMZ@3bl(%zxkM2(0go)ymWFQkpeP?xB!zZT5{Dy@8h#qUc7B@PpPJS`Iv0A#N&03X zEr|az_-A{(7KuMI#i?NHIg0QxEA(`dGo=$CWnCJ0pG=fv%{K@r%)#N(DVn0Fvu+Zd zy4e?ILLH>&^;`aFk|;=L4A;}p(1gA~`)%Oeg{;%X63lTt%CStb=dKTh{pLQ!HLQBd z?M2rkKNp)s=rB5eXJ;FuQ+@t5>TYle)L*O0EXicJRoeo~mO={O&onn&uq!aEVa+wf zEb^%VG+}+M<`#zAa*6^6Q1b;l7 z?>bYl88bcm(2abX&%OMU5)tbuQKrmH5LIaSsdNqO^LFRja_^t8gd*8u1ZmWdFFzGC z_~bsyalS{1_-_1RwTC6t2;;QH-##z&qJ;-#$Je9OX#vJog<=JxiH4V_FltLceuP z-;7H^*Yc%n=V-L$$0)K&1KI+&OaAcYCAD-^Y@{vN5gTn?fZ`0Eg58es+dGu-bKHUp zb_#xRvUVvpIFW@(r_LWpyVBfflV9!3Nm^dk zuaL6&yc{TmDIG`)zo(bLEf~qOZN8a^BLWwDj(XzJV{V2}_Cs*UqUdp#XrpRPd$kv@ z4iyi`=j2F5Rtt;SVPL&Ns6n>f%AX?l&@UVw52TA#w;!ei^Je%Hz+@!puhlNiDa-urV_L!bxNENhKUzEX18L@GpQk$8!ZZo8+&a5nInH-Q3*> z&PJ6)-bGh17&PY`6}J1*;bm6%bIO8nn^3@ArYZL2T_mxf|Bo4K+0&;}6Jl0}7*U#F zMBx&R3Oi87FqB!ZZ6tQo)s<=C1CofVv6De|>9S>aU)ckF`QHaI3s8%kAnrp1W{Q2f3m!bmkw{=8Ix@ftYm3+Tz=sIpn)xe~SGU4H(A}c_ zV2ztJa)K!*cs01hhl=J-)>Z;5Q1V*Hmk1mQ~kAc4oA19_5(&!6bd z*kp@=rk0E(H`e!^%a9`)^)*zIQQ0BYbvj?}Gg95#LN41b3Ezkfdb?vU(WN1czkTY* zA&Xe3n7RKlzS2^+JG`CVAmSM1%f=f&zvg)2e6f8%SN!DoboVw822ZIfKN6Z1 zBU3Piv{DU5qX3qUFJoKpjzvi(c9GBs*fm*g7t#4#<>!0|XI;je+~{*4ctFFp!kYOB zwd1HortjDYaUV|KT@=6?f``4|W|zctAO8fbOoI8`LUSwHK1qG*O^aQ}Q{_=F(T&HL z1!lVr!}tmV>ni(_&bZn>$go$8KEl8#bqw$YEVHM}FdF=gub%(I_F#cM1B{9w0d_p! zoF=JU7^I&oDguPG53n=EDh(o52l^>Bz%oLGi*2LGB6|~=!bvh=k%?dMQXIGdhqlfv zvI=wwxGHk^+%{jW-*3u$S}oQ#iUr5z-`$yxAw~FQ6&PdfA8ihuYQZ;vD#8g^Mw_q< zcRp7&(Cg;W!@QBQ@{ypi2=9?E{5mLo2ui~;sZtVG~VmfMQl~SB1cE<;iT2%bf$t$87bdj%m`p4|*g;CVkC+s4p1LmlpfXEi* z>`h+yuaW&PJzuBf17MAf_Usutd%E3??V&yoALM*TBh7PYnz+hgg9VUrc;b7n<*~h}3G`2!C2whh3z&Iz zCFC=!BCImY29fMeVBvoq3l;!2}3uEe!lYPh`!Mu0ib6NG~ zHe6{c1Rfib_liJc>2x##j}9r1yYk=I_6gVXd(3RLUsx0huO6q?uI}*RC>6IkryByn zp6f~9vsKB6fxJQ=bjTm5B?r;j~0`^L7sNOYC*9NTgyV zA6f6c*tb6??6=izp;$|(szHFk2wC?`&W5G}+xrfi=3$AvH5z7qLfw=22irU)X@P1d_Qe(!YE(yd( zkv^{{u2am)^Q>TDa4;Fm5i`ne6EvSnUBfG@N1sJ|6Lo|U{dRR^L5;rFJgpEeGq=s8 zUrE)pklyJXi5Y!-Lb<+i;9}aos4^DS?1msle)wn|^8v}~Bnm^NOQmL!;QSW3GfsL> z_TE6OIP^ca0Evwa0r~keMnhHnZ+N!N|Gi^c2G{Q=a4m}UpMS;0Ob6SA z3GU?;*Yso;BHFH01=pXhDt(kqDEP1mf|DN0c-z{OT_aGY-yRQV1sb&GvdIq+Cdd^P zmvS=Y%4rsaEIPcLthv4_0-BKd&}A_@!T5MAF5#I}-d&VnaFP6d({#CsTS2(A7bb?YaHA$|)Aq-he*D1_4nO-^LeUD4#i@m6~k zD?TERm}I#DK_zM;%7_j46Gvr%)I~w1+|Ny5;W^)v3zw93_n`UA`W%(5f-|?s%c9(4 z%iua*XkG9dkB-eZ(c;0q84Kd1>)NcWrb96Sf%r~X8pYbdn_FraB3~uE>n_e{>?z3T znSepP5^%^YF~6;)#Bw-Zbm{#7wxz99)ejH3-OOjVT&nAcdLsM9JuVzTOL8BtKqU6r zmYUQIHj*qIwMhz}VsG0s0ibkx@3W{In;t7S-l4d@&48I6C)#o#Wym$nRGz&lQEPzX z--0I@usL?2!z0e5Q-X!lA=A-S&?eT*7F+&*vf zIIRD6&3Z@Mms!vJ^izerBdTtzU-e8EKoKL+KfJu$V)gLaRg-Iq^hf~}M3xSV*gnCw zOHv`XD`8E&G){4*2Y&v^Iit?&jgH$MeGsD__s3(3!Qy0+`+Fp{e8y{fp(-qCVmU?) zLf?t#kgF}9mFoE1YQxV76Ytro2GMCiJ&JprfQA6xe$(-usg)=AA4isDCIN^p$0it^ z$oz~5-FTq;EduGSU?NkR8+L5p;!Bq%36CXx4SpP>TBI5((dd-klE!eOEWw4>fL!i!G@gH!1K8O}nFStXS@&MZ&2SpBf4v9FoyH zh%)`1335;5JmAC@G%gN7w%WzhfXRfF=zeGVcZh;#PF%YSAe~%^T9(CHCjHt4x-TFpEKT1b$+e4x1#dTV3 zCs)BrTw3Ki<>m-4KmTIaqt2Q;o-0hi0#+iI*yDYL?I{?SEU+zvuvMuK(Me%7de@FM zo~BWv8>y=LP_5X--_uMiZ1jb*zRIkF5HeqS3%a;MH>AZs8pe*GrXH_7Vy^@@H8q$I zx&93Tjl{}clr;!VM(dH`cDq)gu=GY@Om2vMVbQFaJ;nCy)!$2>h70@$rFrZdh|Mh) z50-gOiFUh>_I(P}dYh%@fI<4b$t~LKOcHkGYOog~@nd>G>gtf(j{g9GGBz%*=TO5H ztTYCfw_vlmE~b#K9oD!SO10>c2ap5R4zrZ2W@mu(9BnH=V2}xb5qm;-yl1et=xAsH zq$1O0sil?Pc^xADm>ghM%48-7R-nBV^{xTfkTsfNJ6~-wXvfxxZXDB>dn@kX2p`JO{ z@!RZ2jMpb$gnC?CgJgK7W zY!e|sSoeNBZb%+moxcCbP|UqBKYxBVdGq0_C#zGu*iMfmTW!l7x;iq<%Bs4R|)>v15QE++5BH5=v)|Qa_7#Pt2r+%h`G7;%sin8 z5PFm92ux8jh9MBw^~NWbE38%j8*f8=_Y8)MC#{4z6SCX0vpm+S1`cFs_AsM*Nw38; z-9F~5`& zNPU|joFlBLpM#cYKhc^<A z(kA3_fbO+N8I88Lpr`FVK5j%kSuv#-?oAUyo7uje=$zYag|Gik4}pLpgw{j6-GYox5zYK{PNLY#Rc6x_cFRDELP?6CP6p^H7A zRZG@3BHp<`!NK4f7Oi@*xcHV2?ViU?FW}m(0-iU=*d(^--k^&M_HXJcJKv%OpaJ!I z{xI1%dKKd@u*UZSO7i>C+&$z@K-mW0*;7L+x({`i{e026H{_5DM}56emDF~AXew2p z#d|=D^dBnTXR%{`Nw7W`G=^I^vbzjcI7(HO2qx#Seql+IBcrSxalvG5mb;ZGQ7wbx zZN5`I_32KceLg#9N46F4IQhb)S^e7^LHm!+CnYERU>u26&U#zL)P}>H6*~FkUL2iN zpOs!01Du`cKd@p~J?RT9bdxhu^$P%EJi>=aCqKQzoB_uX8|1^Rq}TM(Va(aYkn}ov zI5b0VyYUQa#~(wX05d-A(QV5d7>gK#f*Q8cbQYIWfkFdp$oFuJq<`<# z?O7Rz5Gv|;30$Q19&=V5)hqPHiGg+oEo^lQ`e~^QVeYS=o`!$tt2fv|kX<5jk;*u) z)*nSb92W&Ow~IRCj&avmK=Xe^ri>f22|2ux-WKS%SM6p=yE^fsRTRyT zbaAx}f_IDL6VOC}%#p14fj3_mkj)jDbedE!vXve%345h3LPBJmTwU2gP%}J~Jw-O5 zo1+!$ab}*jD+_ty9PA(j}US(VQ>!53l{T!m7uO!EtHeIK_CgV@M;g%pU`NB3zU&LdGP zVz(T)5Q-6G1>2<vL~*PW{mpwaE_nqThy;tCos@ace>I%k-k)CX&1`uRTR=7njp}VRTy}A}2FCjfucqaa znxfe84-PS1NvofohYB+xl7oZKSL@gL!y4}7e(6Yx-|tonj=+b2NnUq!a^^?-d7Inzm$nF{O%>BB|D%E(0Hg9%MLt?iEmbjCqFnBm` zvxakCjbxB)y&zR|qOX7Tnlo(WLek(V|3cOp0qo1EsDbafBwQ7;{ifg)I5L}H!s*Rk zk$x+VWNo-gQIKuyAW(wawAhVf7*X-%cH@jCh@CXRSjWz7}O!^78RR54wLP9*nWr-WP4TJN@ zziuCw<=O@tV!~IVqD9Nkg{L#9=F3SXHPx678Hq2N$JCjPxja*Aaz*C_bEO?xFmmE8s|4{!VcrCKs8Vl@dD4( z+Uq$pAb2qf+S^NI`ae7zyInYX60}hGzJjWxkW_K$>~w_PfahxaC%_+p64TdGVa^5uGs= zubUgp>0FC1{G6R|3aY|7eEnHyWa-QFiqrm$7=<#dO@5(o893oGTHm+}&EvWI9B`a| zv`OU&t&t$SzY9L&mywkDG#uNT{KH?!+=G641}GcU?e(p zzw;6eZxsnOZbw~e_UKdHp~hjBt9qk0?&3eI(hMK6F|8xemmd-H?DNXT#==pWCpd{> zGuDgaXE-M4d$;VQ5$FKsDI+|QIkfH{8-JLgQrIXCOuCRd_gAm zB;>2{oj1+IFy^l&6qIA94&;l~E~$fQm%x?s?pu*66!mPmX#k8+c{iYv{Th6>h>wj; zIXm_~j%AIT!{dVEkC$;?i67<{k~Fu=canA6vD&$Rt1C5hoHxWJN-r?54*v8 zO1gCq*AY6tv*m6Y_oyaf4%fW0C7-Ei>$F0k4d?S1t^b3iH<`OWug)3iGwM-|l!6YU zW@ROhMX)1Zd3(T9F%Urq(F;4i5I?)FRvtD%BIlz#WJ&|nn zyw0wHF<-33uz0VpXKy;_X3mzanIaCzt;-Uyz4V}y2}u7E^0ooBSj&B1^Sa~7wNslr zUz=QtiLddCQ08>;J$}ZcFAif2tJeeUAF?lt>-TVsS|#X_gzSLM+AP27V_PJ9Ej8U{ z!hJAbOd{!eB1J}J(gP(V=C(o(+$y>}>OH&~zHo_)Sq}C_=qgqe)F~j!AoIffLLX(V_JE#k{P9PnipO}&1_!Gx52u@JVPB5W*E%7m zBbtY&aF+cBL6p1K-$Sj6&**2~r=7wBePbEa;e0tO=}iZVYYZu;!Y+N@j=~a2Yh!p2 zyRecKUy%zxt;%yN}oIvsQ;vW30(oMhma)aEqFMFD zWsgu2NcD1BbFfRFTR(C8(d`Ur6j-tE5{>_Wr)N>E<(YYJf(C3eU}J@`xqZM==tW+y z$rbiw2CAsec~}2lATf*ff`{tV1E?hUFCx(7qC^9GuTT2zFDEkVv8@y8!aRo&-#{AC zn-I7Z+a=d=Oh<$5cYTES*K=zlQmf`>{}4d=WoXcV0d@(9rgc&k;1T%smT(YAx`^Z8 zkX=QfU4RKcv_JxS1uD8AVIj2MC{oQoW=f`FK8TNw*IWGO<-pbowWQ}k^xMVxrQs)` zkTS0}S3ZHxC!0%nA&?_%*536mxGK`CQ(3_Hj*w9^kI};ug|!0%fhgi{)-xDjEyf4jp-*P3r(9rLYw8VVjW>RBQ@!O zviSL%@`O^p=DPY&Z(jUS3Rh)^CDb~cJ8^M<)uYBQF%|R{8hbTiNmIfXuMWTMH`i&e zJmipXw*}3#s-2PISq#DpcLgSgK4`E-+!Q!BFo;v9@x(Oq*V+=QS<6wPcBJZj@+B59#Uo+B z5jdOGu?KHgzR&A!6}r!R!;hC`n-ve32R}%c21J@1)Dms)O{@OJ?-YAw8m}OTQ4u2u zNbuaq$=BjLZuxvhdfI5y=ppoT)ac3uH z97ecS&L@pJ>dCx@7QikQLD+v;287Mj>%Bh~T(!iK$LB?Q0;&CTK#$O>wS0?Dubh(b zS!*orKp4N6s_#+ukI5XN0MOy3-r*>SyI*=LtNMsJfk(3R$3N>tjD>3MF2*QV)8H$N z;qOO)_nupMR=&MH{~LIWDncGG%#|g)lt6bfEPhu6i@dZhpC>tI?Uw_zJsNgvQ2-ZI zK~InALsbW~R_1T(KTLG4HSbFd+i8h4c3iX$aArgr?Dj+ud&`ibLflEAOUW-qQw_c2 zGEN9ta1pH>QUO zj3u&AROszSJngBC;}9@^k0oB>Q_CN6ApN~6Eo`16gyih5H%!IO`(b#QF?ogE ziH}4)@qU9;IS>m*)jV2u{L%MbEL*iW3;r<0v`gz!*QK86YgT_XeAUZV1tcb)nqC0>MzHH? z3bRB`d;eS=a9a9II+kFFCl;o)jAa|}5~;M;D{u8Xo9@$@ZnXj>_v7QKZwKWXpcq*l zOcQNy;EP}oFJ3-%KbT@{1%8Bvc+~p7ibl3g?F;NR>wK^NxFq?BZ{6Z zBxoc!zxb(KLS;P@!n%c~!qbR&0LkS!=y!#OLyYDjkimF64Mq zEmXnt(q=p7!{_w$dG7Tw7Pr>;*j#aCYkj*5XYY=mYNX*9Fu(`?CcEz~l+P5me}?gg zDIE=io?gT^$L&m@9`hgT_+`C~Q8|Cu^vA+1FOL9^Lt*A_?oKK97l)_^0Dvtr^J7s} zBFFxv`zfo9Wvb!N$rnlq}D-rU7;($~Q&omMEFVSx|Hz_)Ei)OB2E)F`JK zO%Y6l<9VBBIY+Pj0|DD+W++wki&arV78|;1c{w@Wd=rL;`$KGDqA)KfIR66wM{Hm& z8YgaW?eP8;v~N9`7f#RcaV z2;%+Bt1aclMCEXFbi}~N_bXCvR?5Yqr5%PGYoc{s{4rQt%|*+)rSfOJp6bt&#kxdd z&gva?-4L3%E$vkT@yX+tNE*0kP`uykfT07X(?gBLLBP;$>!6Szfjyv9bTxJ9;;ArP>@P;bv#cF_6@=@ ze*o|lYzAy{SAAauJEH=lAoc~}mjg|itH1|sjCEei6gZBi+BfeY`@<00 zN6@V(xt#>#YP;{+UpV!bxcfy2WC;~Q-f^1%X|#WZ8;gw)D09yM{NWQ`nJ+Tkzaq^8 z+gj}Q#y8|myn#>4!`3_-Dw@60s?dwr>7lc5(OT(udl8p|MW++6a&%5BYn;z{F9CYi zCei1?(v-RNd%bKD?SJ>bTg9cytp{oSfhyfalM}hK<&~8czw|EkM=+Ye+!w=^S0AGS zRo@>}e3;-%ApP+)HqKQ^UyzSsa)xv@v-t8;JOWc-Eo)A+4}Qg;v@0sAe7fFN8r6!V z7^Nh#{|~vq>F?xvL6>zy&%@)_vnHf<_y;`U3E1t_ex0K)55ospOkV^|%wS}Fs9fA; zW0^|=FXVv~pBdP`-!5A6x3KJ`#>K5pEAKg1@^vx1@s*BJ=3#oSHsv*l<{(^y+>*gK zN5{L}c*uei-TpU5CjdlC0k+~qxOLP(w_+y%M%a~~>|9(t0QQb*{kql1C9s=avvavW zdq}&_aeZqAQ*7zBU-+tWU_5984?(2+91&Fep}SZg!AtQzM5YEyh)G7sCwm_(M3(mc zkw78|i&!9H?PDJ=>a6@z947Ox_506gonUdnLMQ>#6%U~}4%ripo}1Gn)WDDqb(tXG zm4eqPPFQ+r-7vUiOEL`4gpO>57@X-3;)&0KVgv0fr=kbr*|;-BDuM0x47QJ_D46H^ z__ci=a2csNwu$S!P8rrNwZ#zTzHWdUG%#Dva6H4qxs4|PJ$YTcO_~F)H7v?^q`YBx z3`1Gz=ZoQxX&^3Eq`KpoM!UB^MfY&c{iV8e-X5i@9De}#>bDiI*^`{Aeh)Cs} zcFpTApAdq}i*&bDd|0{x`a#?&>b?-8hIL#<#(uKTOBcg^I7^!;-GKahylXPT;Os22C?%v`H@?&uX zY{WrDeA`2^lnPptZOa`dkho>>aFuM_t`P$Ov+e>;P;4J-t=KUh5Ut}1HLR<++&COo z6NIZ0_ZQfMT)19nGBI!-fgaAnzkjMz`?cS{@q^{|M{K!%g)Ou_qo};V=U+W65(D>| z;+pR(Lc5OIT-FN^yi(3K{wCS3w9|5MO|ppgd^CEPSbyAzk}xBbu+-WjjklTP?o9qRNim_l%RV6GiC)lfp9opa1;c>ACtB8gZ^%b zl!@^Fr^41{*=of8PlElOJ8((1zwWVk#^(IE!7fWCkmkd9#~zu=6WM95>$}~p`j zQ!NU!Lb;er(s@soZcH~P-3yCtOC?xwh0wH}_NGIp=T=X<-uU2Xbjj6`aO|(tzvjw4)Eg3l{HAe z1o8&3@&}IFI#%jdo)I9^MKT2MC`nj&-)%@Q`zCBhArOKVZ?yKOJNj(feV%DyO!~m% zFrhIV5KTWc$t0$t*Z1`GPC@#^U<%Ohf#%f)gOXMEpLho2$zQ(?A~*UO>7HX* zDhLVr*t{8uQeB=FROtswvtLyT$0lrIrYDZ{KT@n2I=S4Mq3!~Qm2g_1x(k0Z~FKX&tNTVtM-;z}zGyRhD z2JFP}e1k6&jlH>d)h%6Q z{O!Nmz`Xuwd6Luf@<7q0jI80WOs}(iJZWKxC7m7(1P#sn`kryL?ITd3PCFK&pP`b>P>jUTOHj zJS*lH&hP>1`=ZwVA`6Eg+2)-YdYVTBQqx}Pvf}7*f7qUa7HftMaTs-}w@&CjM$9++ zi;-M5*Nme%k<0Y+6GpvWSNK+C6Gw^i<(`tI(9KqqwcAbc)4LqMA61n1x;>dAfy6#! ziRCxRCo{nW0VK=5v4M&J#EzJFoK1Suvri4^T}zS_x&OJrhsjkRuFM_IM)@ZL;Y~EMe#WCw8 z?2Z~Fl{aS|Oj$S|O{W8M8=FG)5#TKRr53r!~Re zt~`=(I6K=2@7=p0#V-rIz6Twb)qwk(Xan7Xm?y{Z$r|=Y6p|zF`8F0tIA>_#dAntN zlw~<&6r_e{yCQW>!W-5qN*v8jPGHQU0~o-NRoVJkgO5pF(w+$jUU)|% z3vi>XO83(!Pnk0rpcN|kiBeG>m!7zi&#bfuVk*2()#m=TA+(EY$WVi4M5&6 zXRyP@%$%wNR{LIKZ;&y=^qPAQ$i{bEw)&K&5r&!s2Na08?cTv2GvXANm>&_xK4kj7 z@B+nK-P=U?TRX!~`NfK@yPm|Se>OksHkX&pPX&d!JSKErJBW296Fhc3O2M#?BoC*p zsTPm>$85j2@r>oHa4L4q@(lVxkc!pT$IWHO%{3anZ?6so`Ua0=w)>?k#~4dLmIG$S zp9Cn-I`wCV%Gjvi-veR8-!D?a1}^{JhVcoaLuN|iQP_i50AObSJUfQ_YNg|p4K^HmL@PSg4FPg8h1+o-jP5mE#tAV*4lb$nv%(&*^TXXH_ z_WtoIRL#x||7{>b?-H$$=Y(%tEK?P>`M5z!7X2V{7`1mf^zD#!#?{7KIO==3b}rZG ztJz0A4)|V;iZxlF5Gen5{->#uI-R|tGA~+&Wd(oOg~LyoWT0>U)5ATiM7}KQX@#$cvWd#*Fw%zV>31qG;uk zV0!2AcWiG_gO0qC%3L8H-gLs!TEnS`kCbv_z>6}8QS_;QL8WtY^_B2MJuv5(ev$7` z9%rIW0QSybeM$N47AvooD=QogU)Q`S#v`=A@&TV&!(LB)Pr*^*gbAZW6aQVq7)uSW zw0#h3AlM%}6vreDRHQ*o|4B(SHAGX?*^%W%^FeTXN!^(ve*icsZ195@vM*cPgK@+) z$$&5rM=OESp=RZK2cZJWTfoB&Ne?*Ryo@g{ZE`rv9w@Fa({^FtlS0TSlqLY#++5ho zW%_*o_3n>nI2Sriq4^k4@>{lQXkIKzFp(NM_*`TY)MTuG_0EJZGEL`SRp;B-@(V4M z_n&oTf%`O9t7cbA;kx*koA9GAqh<)7^O5YcTCi$r%nd17*c%zOU-X3L*s+U$Jw7#g zObNKJ=1MC$;Q%8+hYnG0qo7gva~{QBibbIR{^Qf3L4kq(FsC!1Ukh zThOmjy9u_mOF>#%(TGS6t}sBr3X@xAp?UO0s!3c22Q>GBt z^@ZBXfJ54D5=`u|lHDDgoKRn-pq3qs;>hg8&yO(SDu0kM z@W6-&WS#x?S2cChv*2t|y~>H{$6R@9Mnam)SVk-0A(%{)6Jlr-_HajbP|vEG`2NV> zw$|iAe~o>ZqFAD^|62B!$<&&qt0me&^%MC{2qFOrr^a7iAXNFfk~ zI4$v092MVVz9{wFUy+0a2=w_Hz9mZ?{yujq1VMQ&w1Ke{UeQWI_g@91zZme_Js~2@ zqLBsNsUZ8>+8keF(gG@e9FWds*j0p=leAmh)n2Ey(i zZj2<06fC`v!AmY7n{{ft(FvC=jwbw0;9JuU4q&Z5h1;DBu(gOkr8@$%)*bn6#NIzM zp<}HY0+p)Cx&gfiHD#lQx1sAE>i;BMF#io^Me22paFKiK31=sY?=3wA6-lDG2_pD^ zY{Ef40L;u21O`tu&J|dN30L-HQo{I*ogABAh$(ES2{l##!+))<7qG>m^6)@XX&GM; z?C}C&=OH*?IjvsOVvZ7RpDZ#(F=`YK?sL^%I^Boi|Mh}A1$gKMu*->7HAu#NLNwu0 z8JPp}ob#blFhX~Yy?%0ma?$%oRzZ}@({_p+3ZD|$_}+P`lbgJ}HuYLfGvsSfn(5w_ zt$LF)Z%Q&Ib1JyY*iWrx>H@z6@bpH`sZCD5>=+gCshwVq-H z#Pg%qj)SVZL||KB`$LT=p7C2BjXNeQ)?;jC<(t(*c-VC=AwII+(K`u?AY15qguegLqhGVJ~sjMq9Mx z-67>Yy6|WU_l%L2V=W)|MSk2s{j6ii;iBd^tvOA{Bjp~1XtdIl@&nPYuyVk1i3}`# zV47<=GBG1#eJV*r(l7G)1DYMNFv*_g6_eRec|IZ+oTjW$NKJc$tw`pj_b}Iw`F})H zrPiYR2x1lh7W-eQC*w`Xjb0D0KszD9*C#t-uG$X4g2hNT=#YeJ;dNGE$tE7=H{QIvNvOq z4#r)?L4qGnf@^x#YjJWE21k0-%cKx~*epdDOOmLpMLAo@RQ|%dEB?$Ic4@EbWRFU) z_5L%-sjvFa{*>>&HwR1^qXk;D^#o-g8Yb;p#CxQlA)K7?0xj?!k@No;*spnK(}5b^ zu(qcI>(xu5Xtlf(hR?I-At8$1vzGh2(m~c)l5F_iOH+LP-)x&W zjh>R*yPg*`E3DXqvy$X1cT|A}st3}ndw;dLy=XA^-pCHW^G%pzqxF^AOUAjc_v!q7 zYEDL}^ZtWi6eScx`9P9>4!{B7e|wougZ;n7u)I`8E@v{LsdE0(-*1L$q zIZ)eTn2;ZizL)ziBO!+Tb{+vNe3Vf{RCGI*MSMw|D$O+qHd@ zOAV(^+r*edZ{IRJMc8)phX1ODV?gr@w zr9nDHI)?7h6nDag%P&PUuwk0R@L(f@6BEgSj%5U$RQsz%z0qK;+B9_sx!|kq>2(U& z^kha2)z;gUfw8d+0hg$v8)6=3)6z12^uz1sD?A6uswgp@O8gM9?M^cOudm>O7YD2) zUm3F$Umwz4f7bBjeac=8&oSDl*GPPUu@!@N%!otJH}KI4?Fus&>MMd9LB1gISksoE zTC8xDLgjd3U?PBg_pe#%6Xr zO<1mRuB+W%0OxSV$tnJ2PSfe@uA=a_Q;Ba`c$V=h8EKsgBu_vQC`_>%lC9(J{sx_V zZ`i(-jD`J^@A{{p0U0kZ`^yUqi}#v7F<#MdZ}VXa3a|(B(nVEusFj@5xw<^&RErf$ zXyj6g_I759UOv4QK<_(5-_(5mHe%y6M7Yu6^24v99v%4Y-;05lYTOr z<&p60UIKuAz>CYMFFV~yJ$!^t{vxqoYP7ve_KEqU43HL~UM;$cQGdIDVgaiGq4+xa zCmIo*#4?VU^rWB9dS_$w+5Xr+*wyiOq17g-l(RvXV}*Op3yA_!wfoMGU!I;iZO|F( z**AI zht+aC2oKbl3ZWEQBKZr-Ak?AYDW!6o$IJb%#q;Akg2I0+N&t+!ER z_}+bvYW?WBuG|_Dkbv84sHc6WvNtB|0j9k{IMiyj69(7|sg?VkNN|Gwl7;3?WeaP$ zfO4b$^7Y%tRWTxA^FUY$MC8mj*oms?_rFOD^P|<_J<LNvHAhJ6?G3r$XERty-LHIe01xr!I@h*$TiY`_hFDWwBXqeE=KYxIaTxmT zPDxQR3Yo0p% zeyPmaU7PMYS$)mQ@0GU=@obj3pR>H2lfjz^$VQJebf@bI@F+PUTTx9CwQ$8*ceuhRHskCMXOF-S2&c{5ohdR)AqF~*{sCnr75x+R?~;TKq}vklVR5l=lz?swCoQ#y%F53;r(#C@-{Z2HTr$Z@(>TVafCRc znWE69;tVn&>F&FzL!y2~p*P`!^@`;x&q#$65v`~}|y?%LI~FilQT1Ss9Jud!I__o~kvbLV56 zuFy~dbLG#-Y5F~spmXM0rZT~||5+vRx&4w@LIwu5=sC3qPVU-l1xE1 zUqh30ZZEPuDqxLU+`I!OC`+AOKAl5tKPak2`EC7pvz!vU?=ru_>rCC&u#1S>b-}Q( zvz+gJW1dbU7lbw&STB$4&akk+=zs;vPrsm~+xAf!yqZgGQMFe}BI*}Zx!eL4n@n7W zFTwd#67l{=Tg-ZMgpfu)zzM2bZ^j#$#UF*T5pI@>rzuU<@b|KX`{^oox-+*l#5>#TMEXTQX2EVO zH&MqY4;qT2$u5XDvpQEbNJtS}@Vwo)t`JJ}`JE-G)P;c}o1z z_V=usamIChrLBf4DQK>(Wpu$-Y}osQyk8RH7dj^%uPbh^$Y7FRGOu_A-KM&_d(cY1)V1LEeN-0D$lM9783TU_9; zg_E)yQQ*gmE*Rk;9)-{`;PZwh|*~GG%|R6>@6!czWLP>-i?%S}@at33iXE z8RJ3xC>OFU-zmbz$x5>oUbLkdD5v#nE#WlYE9&?#Ve9RYp*lrWWu?qJ=Mm9wNFMAMzmk1tHtJ{%KW3_RQWbAZDZ&sP^WsP)r&n1Kd$5}$GWMW*$*TS|dk2{vboq!l&m2z_AO+C&gYqtX%rY_5kZ%5?CHH|oW3HnPtfp*y^y(OzdK3TuRlujBVPS? z8jcXL$^v|_Sn+5szByO(X`T3f0<*}_;!LtENLsn7BdAnyCFF|%34!~m@FxvB^B4j! zG8R7?$&=l4xaQJ${5`+$p7l{p03aarFZ3pAnK^BFrx5#~ zt4B5%k*0_5Y*_!WPd;T|V`W7WnoBJ?`1ts!iajSZ>>V9}1rWc~Uh?_DjilypzX%uokcapHFrTSnQ!!$-nN(s7Bu;j1!*52&sIy&N*D*qO`q zXJ^xkbpIw#fQ~D{1}!qI{(~ez{t}LI$zR|Z@QXnd(Cri9UYYcmt0HM$i=+(M znWKTzcJupxHuInNA14jFHLn5CAZvRbMGI4^zSOt&rv8}>_w;+2e_K(>|3gGw@K1w( zw*;Wnes`z`{JX0FVeRk6=X_PYG;B<(R$CySsp{6I1BDKGPlc4b{$Yyzvw#2j^X+7; z@7RT+<5k8MjyDs?H=^D5}e6mM<>K=fCjYSzeUvsOa zzuWpxGyn6$J!2%$`2)Aj7nS;x|D4YM<7ofqfBZly>;Ig(A#3ra7s-FG&wo$R|I_Gp zo+VIUu3DZoqq-)Cf(v@~KTi8;7jNbxtVdUtbiL|YE!1s!)im6m-w!30fcMqL1`v8E z$FgH{w9i?XNKv%fA(_)xe2>07f@EYmY$;?q4p62(nAIBD*5dyj#pOd-zU@D!1cYX} zq_8Wb@h=|czh|$U;}xdh?rM`y4$E4T1b$VxlW@B(=L2Zvk44{{9;b73*_o+(ds$0W zy5$@i^+@g8a#)bTRm zAJ8wZn*PjybK-ilfb0K!2H5F8$lah1NP(;7%1X%^C0%*2m_t<&>un%NcYzBX3b(#~ z;lBU%((yO{PcDC-n}1ucYA79W25yRv$#dM7y8mXp{(JM(C_7XduImP*SJ8KQX4tLx zbcM8Nh?9x<{>Gd-T-mm32JOKL``pSiy!&r^2^yMT{(bU>7)e1d^8a^xAfskiNL~bm zZ!b!In?dk^DFKT83MjXPRD%XGluQhnXBYpUJ8keM&7Wlw>krmM(jpe`28d-~09ORM_>;apEHU zgGv6oAtnJKE58mqTffR%bXon&G_Tv^^p4Vt)K-Ck#TV1_r;z-0U(&8CFDxNwVlk>6 z%K6h_4t(}jcHd4FeD=?qhta8}zK`LDtTPrs7Jv0hL1xT#`e=Hs3fAL=YYWnkEU)^v zoBj@T(?XsJghGDBc-DV+2&6@Px4c}f=`~XZ#eM)6D$disG z+gW_ppKbMhxF!?^UA|K~GpV$b%T|dAUG_@_JD-SU&-`N5DM{l<(w^){WgxHzyl4eF zjdoL6g6?-v`vfD#s@=-whjo97z4NIOrHZli@<<`S8t*;eWNqe)ou=Y|Ug+##U*yl9 z0NS(5?k2b6X}jzm%i7&Az=%BpNJ+k1X?EpW)@y|pA*+3lNjnRSX=g8LhtB*j!0&$3 zR6#1ME=77;*lCANR;f#0K7)TgFjkFCD;a3DU%!W>t5@k16pYXL6)R2!0vlc4eAdYZ z2ux(61+5VBsnJ%9gFe6>*Wvz1MpOdB{r4gReZjfz-;1JebLG3$ry$^UOQP4}63%<6 zMY;)-vcMMdL z5zJQ_bSheavkI)|Dz92c55(0w?5E$vEl)p+IPWsuoUX*Cuo}eM&HKK5c=i{l)NOvh z=$GAZs!xcnya&AM0Jh6$RMXvswiZw#-~&v>B$Yb=d)&Ow>fww4kCOWCxbANOe0z2_ z%Vm7+YOE`cpUsSD)iCy>gg@9jS@c@u0d3&hxs}n$+-Qq#?$qAL$yJR8Bojighsrc$ zy*sF!6k#57OFX7uWYK45h#SaT3&eV9RYo`+%`zuK`tDk?Mf&tZa@Tn67C-zAiIkSI zOfw6F+(LcVi~Pku_pi0OuN{;0^R?4m+bBLr_#%Nl6r-|$e688z7&AA4vAm&8#-yus z3`r4CaInX{-ALLB7zUr!SEZ4%_Xx(@yc5;SS0zzW%ZsP*E8pinvWhLe)OI^BTSN0q zxV*35MOc!{092{ZP*{omaq^ObOX9xohUh~5-u(i6ej8Z&C>1kuRqy!%gmG^L7f!6t zQ1l|&e-FDf3?2P)Sg1|ra-J2^ZaT;R&g;jXg?BbL>3+-6MPHr{IF;6YMsi43kh$iJG&%J0EMO6T1t=$nlioQ82?RpP}lO@O_N|uiE zAlL{NY@3z+RGM25d%*RTuJ8(VFr=m>bLpE#$>lE zhAJ8q1o6`Y|CIDtR8B4YbpIp18D0wEhr2{J{QF!3{oFraay2enC~RiHHN+uX{p$i# zI6Wof^4{kA>w76astT7l+cZc~agk?&69dGF;2Lq*PzwUT1l0#+Z*MA65%1|^*t3nE zP%g7Wd0_Ui5YQcVCP*Ux# zkML8O%~uN-U0-~59~lcoV9Ks`nTt1i`Y+G4+0}=od&B+As=O?R!NnF&!_EJY7&exYwebE`8U;`*!u47GDzohc>yFQ?z5paFE z9=3blzzeyYDCkc}RibG2gjZgwMz1&SC3-eWKf*_lT4LWSPH3zWZ3cyW&fuY}cbXyc zOWFX-CRM=q) zK4>Nr^51xi_^mJr)@ApY|L22NOg|hFdW1WnHKeyr~^ zq6(0O(8O=wZ}n3w^rKZ>>(Tw*Y53-Es+gw%efy03Q^hxtrD{iKppb)A6Hzgf?_+$H z=Qtpb%f(&RO(47u+35tJxw{hOfpWO%#Lc^{%6h#y-j-PWhNk3|D*y z1zx#Jof|bTQBZL$;-_esYK02AE51q0X~+_<6Qqs~k8IQSW0^ zz)w=L=xY17j65#`pa$;Q)q6qXfkLofE~g8>7#ojt5qt7#*&P> zb<$=1$z4+5_qQU&tC<}V{ycjI1y$C)6NebviZb6K@7`K!QUf8+u?HNnr6YJ*%Q;V5 zC1Y|N(y7E(I*l~sofH7iSqSj9O&ThI4_-uCt=U-S3E5G)U|n@Pywkz_fXQ#;`9n&~ zH_!m;J7_0BivOXv6hpQ6@iX?L`Sj#W5IU(q0^{RM`8;S`Ni%Z7ez!8&Pw?HCh3`u@ zmwk6F)70cPcOuq{n@n5hpKTEw5-iGV3#1=keg*LTL}hm{A071~g3ERo>ec6K^_g0) zH!`kT4gtg=@f9zF!y=r*YfM#if_s{vmp90V@5YdHMs*>3)r&jodd({=BkjyOzPLv| zB%EqyMdTwZa4}~N6b*lxS1vo)Okq9S5J+cJDW-$cHj3e-5V*=K^ z`+R?iJJ?UByv&;n=)8<>b34qAcz|qd_RDQ!=R`6E zfXFVXVeo21O|;RZpM`_x^bYDVYh<5TVl zQ7Dze_iJ(7lH!M>%9eI<^pM0sBjd=7j$+NYCB{e9PNY==U)WWphUKdsE{ z=WMO=buIjWR+APRMQ!Js!lP|jhv}+NR@e9UC4FJz11sr1kL_Os z-U?gpS7RQ#6iJ9)OBW%|(T@UUj1S@fHWV6u|~!tZoph0%nAz}clR zgLGBs)uQ7^tdLkJ^m?Y-(^bcqL;&V=IPk#OX3ZOdz4qYq;QkY&SOvfzfAQc@j@b(R z@J3TcH(V(uoo@TcI7E`Iu*bTV%gB2+QFP?a=lb|lK<7)7?~r|t`l$FOf^AtA44LqA zI<%s^eW>dynz&IJ7>D%|I$i`DQr{icTiyRHi=z3gmjhn!09_TY4_4OOF2xK5y#7J- z>6!!VAF7Zol;!Q+QcKxJ#fJRE#$LuNSfP$^_vkK@4kRUCgMjGEQRcJ7!sTaR?U}4% zt$KpOqtcPy+jU$ET8DswS5rsV<>>SF6S+_hlj&q{WGaKxNu+;=##sGl8R(8~U<^2` zRcwz{7svBA8{o?Ojc#2ffRB6f=J~9%%`azi>TxPFzqt*A(OD{h^OGmEyWVW6@_o^U zpB`Ze2T_o>!__QvG5fMP9(t0X=Z9dg()VHx&G&hiE@K?$t3Ej7Ye8AKj6?PINInZh^-UP8P-?LqIRXCm>+c$_WBLUZwa&*# z5s`Aaf3kgWNsE8j4n8*ma&5o|T5zRJdh&}jcxJ1R1N4MnvsUZuG}zmlY`5H`Mbo}- z=6W}`0;iUD%?L*Ggz0TTr^yAt@PGM{ln5#I45HkRX+m^(K*xXTuxeUrt?3_-Oh)= z!BPq7({h(=#+~1>F9_Z#e{m6lbJpY}fWpushDu(v98SIZ9DG=!ku*Uy@cMj0n!L_L zLbCA_4-XMMSO%LWqCPB|n70TJ476dL``w*mer4G;I-U>Ll^`WsxZ|Z{PZw~lH56d5 z@W;I>Ok6#VjhIswHGYAtA&`z`OKhwdw>E7^Xt}xE`MG_1+KoA-DRE71b>4A}vKs***-LW*3@$SC9lw*=*AYoyIh@U^u>V`(aYHz>-8_*s!aQAdj zH}HEX9X*VvXabY)d68E5bllMOtD(4aJk67};Cxi6!!RWWos^N|OCW<82@m zg9K>})NHu@$A=~I`#z|OJyBQAQzwxS23l<4@Z8zCz1{M&ERv+g;=`$QQG2U_u({!v zh9anlcP!ByOmgSbu1?GI4)0TQi^4tAWbbgpHsr7YruMyUxSu@`&xGsxN};-fC(=_F z;;($aJh$m3e`UOR|TKY)@dO2UJZH7K9 z<#;gLulkfeNxeE0(unoTZN(sCbS|`$-cmk?!^LBzE#Secw}pr|z!7N^--~cZABq+f zzqv-?a(=#rYk}M(I@SBL=Nly0y)Qzr?W|g90h_3aGFrXyht6g_5?)s^&3G5B-_xRb zyz~~yX~W!tf`#zo-O9>*==q!Rg#qdNZ*GKn$X`#KtlV6irW@`h;@Cbu|HKO5NUIzD zlH!@1h2Hs;KpNw~5|l#-AKmMEJpTq5YII?SsUdLdmphE5uGZ9!v{`I? zNQXRrzTzQhH+(Z^PYOpP5v5id8oVuoge5P^wQ65x@m&7=;9G37Bc#9K*H5PR1{`%p z8)|^d=)IL(cF?$y@zy3IJb3t_$O_3V`~3VAx&Rp4a4FQx*F+h8- zOAyc;IRdC%xCl}mevh|V+bc8xXLwKwfiPj**D1jWEBldL5acL;5QW zRs%Douc*eIammTNy?@$B-_N~qJqon8})HUuT9NS?u%%rk7*N z|2VVQiJ-owgB+G%+kUIR1)20+xueIBaam1rt!i9;aOunlC9g^@G*OV;HNU)lvrSYh zAjMvBfjD7B?tKm$#3n%TyPoiRuJa{1%S|k&ws>Kf{ExIsOn!V6T z3bt3D%j_2o4>z$#Y?-thzGr5(5B9Sr?Xqafx_=QnT9xQ$U||miHTfCBTvVIG8d3@-m`Unt|L>Wm3%C#n}>=B)!x+2<^Tz&-t~Dxh!mm-0c8y@J?$xZb;Ax-FhZ zax_j)rJ8&7u<53D8x(iZq6&jKqq&VlZ-JFv9_iosBkABrYFd_bP@^D>{^WR*>6LaB0q7Lg6z-nwdHAZ4y4vvO`ibYE|2}OeAclFNr{4IC266t*)Fq?;n(>B4O;bD(r z8rC@k?`Z?DCXZ0Zk_oez&RUU}MOxn;H-9iEmYymeHMRJIgwbK#LMq^$6erE&)Jo65 z>9+jtLOy_C%`nBC%hsSEbGb%V;Cy9MH&|B(Ggk-mlOr%!9wXM!;EKzIDTRW^vg`ai zt}Y24%u4_RzIX-%D_Ue6xR3A_uzjAue6XIO-Z}Kw5sz|d+)T|_97Gn6S#9z{qVf$I z8un#I&^-0nCZV;ssQw4vW2#y1z`C7J>71f|#c?aTgxe@+M`MZ|meQRD4)U42ewf2p zd`l{UNQ$*!gGrFakl{GxZz|(b1ur?-{Rz1Nd9R^g!6KGjSA)k)z_8A_^N*0pv7UEO zX?r*t1j91`K6-`_tyn&5OsIGHcf2Pp1hd}bQ#`ZXS~In2+o>^{-hg~mRbo*PpP;u# zPHs6yrX-iszE92EvIlEO*p-mVHEGP-hdpjP$-)>yPVr+-))Yr4=~%|b6tyhQa;u4D z&`3cwYhK1mMeZ?1pDEong3r0`M07AZo8>v1I5CTNu^Ia4=4akR3ktc@g}NL$;a^r3 z3@(k8T+DumK4q@I{f||-+(o#*er58A;k5wOV%s)*J;XaoTT|xPsAm%5vuJ-1r^Lh| zz!|$jY{q)L#+bRMeQYutMrY}r(JGht#+T7(ZMLk@OaVt5>Do~3R9FeSp=+&`(wngL zt>>Zs=@nT}UDL)g^1>ImQm8BdtN&Y||60p6FwQGWvy+)Vjr#ifZ3M2fGjMx2wW*9< zc%8M=nO>73W~FgmoiSMss_oAg%wy`G@}KJrY{|h_rJe+KbZJ!Ybj-?7>Qhb$;P#YW0677S1h>Usb?($vECE~w$7ZhKoCgDe+SBuLr1G|92HJ?E zxX$!eI=2WuA9dn-9g5APNk%QATbSoDsxh9w9WT{J2hXR*GoffCr&?)^VqQfCFmSs6GfK$a!NIhD-);r#&S#!A z6oZ`6U8*ZxNT2~p>vcN96KP9r3lrhkqbB7PiGWKOkW?C|Mf13vwH5pc-*D84@zm1f z0b5m6ktK*WNYd?60zC#4TKDqsO8w5&aC(!AG=})dSeG-vQ&N8$uKHmwqc}+~KsP>a zK4a&t5C*X}c^-M1o0B2G-CdYXk$mP0LOWe0);UR=?bA1gk)y&&LafCTQF0+sH^Nst z;OmvxPIcrcI6gmP3JyI;x&>;ssP`o{=kXkDOb)DfNXg^X+MO?AP!BNn_ni_!QYu_S z^{#67O9l&R_-fog{=zH@q`W%IK5BTDpFxk}^-zR1euq43qh-(VKY<88_0L2B%pRBFgEyif zuue#WQ?Hdole7a&rEoYvYAIMh&Tn7H$V%JyteJ zAHIzP#Ex!f0yv}6^P8Kk27R;D-0H$xLa`G;&4dzn7-@xD@;I4=1rtJ?eoke#qiHL8 zoc*n#G{bMN=8Yy65@D5HJK1On!nA*VRgg%zKLyIGlQF5Zbtc|yInhp{PC&r zy#W1yMcmj$C-kwC(4qG`Y?4HUl$SK?Lly5!Ms(gJsC!K`PIslv^5qGuCPr>xVJ0;y zYLR`u=xhajOWe)v6d+Ix?3Nn5F;teCRZr&0?J2xHP-Bhb$%xtrH3_+FwP1b z{5zw#D!tZV&8qM={6ZrmK%#lAGFrTSfJ?FKf_Zbel2gYfpQB|t-V{#!t4l#)lyQan z?rCyoc)C(gP#o?a)zgG5l|`o)F)wjZ06gEx_KnxtN-?XVJ?Tr_q1#)>lP&ymY*3|N zHydnRk18>U1x>~Qo=`+6bc8w61H;gcMQ@TINSi4A%HDJup(7}-9r$+AZm>a+?gU^Z z$}WxYOEroxRPh_XmEn7-x|P08+gyEL2XP{TYf{W0tV067JrD}eN!(#b<`=z`4V7_% z?j9&!=R+$_gyz#0Yt?jW$W*uhZX}VYZ|x)*s?#iwn+-e*gBH`>v^>XGYZ>wG3FM|% z{&4FT$G@p+_MYgOgi@yZjMo65rg*rw^m}y;UZq1*Bc_``WmY^j}u$>BsD zxk4I8P-d?uuiaNkxs<9RI?)$d;)c_Fbz7!VYh`yty`x#{5h?KC6f!*pedfprbCWPk z;?G&TVhjXP6wGTb=P_^D1I{=iZf?x8oFSjJUuR+a?$*e!Psd<9x6I9_D}&IYv-qL| zB??N_tf>WjHjt(9lrq>u4`(aDU65mb6nd6CI&OO-$;2xQ&NRmEIboz&6$>2=9y?y^ z)d&!s*z9smVsqB$66AAejJa{8lqHPe1)*=N+ZGRt0=srz0c2xeM#d5qI#{*VuX9ma&u*o_Vm9vPe-06SSd0&5My)e zql{2vs2}u;U8KoN#8guP};{Fvt$aaaqotO*kzM=O>cPRw4pA&n)B8s-`>O z_QL}NcFkNBI;pTZiABh1pgo{*K-;X{?1Tn!UT(VBT(16X`q={C;9}w3`!qHBF$}sC zw2keVLM!LPCDtJDr9^0KZ4@jBFWm1i1N4k6(TXNK*OX0SD zJAZ+H(oV!4YhLWvUq;f|MaHDCz8%lEqP-6a=7caDuP5q_@GRMN1}Eqe`c^SLPJcA~Q+ zZ4sb^$(%9j71|O(p{OR`gLNsz=g51#0e_roPY9Y;!?&o--Ban4Xy#^lydZ@SF|2yO zzMGF^eoP76gKOFmu65y`db3S?P>x25baH(l827zlUlO4%5_h2qRjB>_^$-P%3PLW% z!BAd-=j-N&6g=-AQA$5M`&(7AhyMK09sh$vX%8;yi)PmH4cw}FF?%hPYP;I(l?o6r z|9A^bexU3Zcatt;A<=`i47xHAhF$z+8+z5s3ufHCpAzVsuF&BSl;0%u|B51q$Ns!= zJcj22#37tgFu=&qvIXvH;81j(Zufn^_yBxI$4jQ2LC=*PvhYl-6MR-OKcc7we}~;l3sc=@;FOu^ zm%wohV#knh(A9=FhRd zJ?a~WZk|!RshOFC*KG>_QWxy}>T0l!jjh()w>q?2Co!?G&&9}I-o@)RqrHTD-X1A@ z1+&OmF7#KXFwY^iY~2h+z;u_cuk5H_?wWXP!15YrbK^RGkue!<7`GMESdI8d#<`cN z_lIfz%(SRQt;+}hwQjeo-lAb-(EyQy(~XZEz5zDa{wx3m!1d1kbE>p?t&&LDM{>XA z5G!J#ZiRuW;$l2qCwX%znD)7+LXoPO{kAv9sJeA2Z@?!-avC@-`aoXWLP73uK_k;v zj=n~k{OjU~WVuE-Rvuc$0228umUkH=8fgmi?eABYI0C~u_U*kEE@qdR0=6Q8;;(&E zQ+VQb)H64n_r|cV=HkCM9@6i($h#v%XMA_duY}DTIa)Wgw>_fY|C4wE@7}BJcdLcO zav$vbn+LI@*?LZ&49L$uGjLG_fZgZIbLre;BX&TcS;HgCDVdp>3m2cDBXdaL86k2P zQX*hg)pt2bDw?R@qP3vz6X{9Ef6wph=v68LxU=a3mwCY{Y4%j)qdU1s1&*Pg z-uaV-`2L*zaxoCop8z@_tEm*ykv7Qdjr;3#6amb&)Jk1ka7B^UN1A%uM>{|%h17z% z4)!%GZuRX^&kGwZC>ov%8wCXeK*|IWZ^*S*9!U=N?-;&Z3vfwR>#bvi=m_wwWIn2X zIy_^|3n0qOye<8xzZtYWcD&UEO1@})8~xQQRtHqnM3E8I(0J5=nNL5b#oN7^byWIc;a9(R8UJc8raMJxE>X2cBL{wQ2a#TorkY%WC(lYO?@9&2{iEm2396c!_6>VSPPowrPh3-OOKs<$`L z`Zn0{99jyJLwS!i^d86g7iImAb^b*%QD5Ze9acY;$p{TnA!xL*v1fdcDgon>2T~L| z8`c_*EbT0=H4Gel(#5RQZsr@YSDj&~)xX?LB&ZhyFJLe^KP5(UPZRvinM`BH9&8GF zBOS5MPXHv)`f4t64QF+MULA91S7G~M+AQ8w^!3hsWLsNnHLNyKhji3H9)^frDIZN~ zv_7D+MVtE@r1K6g(=3AJu{K2yI-abC^LJSdq``SR9bF))n8Po}z~D3Abu!2Oc_cpP z?#Y9{8Xr-Iyfe-zey?Ty9*p|Ui+=r{9I*SiR5v8aB!fPK3Wj)Q1Os# zGj@;^=-5x=J6z(;Iv~QlBRYR-6Nf{bTF&dqXC^0#=Z+}IM;-0Y?;Y0129mb99DVHO z$2g8kxJSatwDM_rg`=s@uqzT8@bgD&zcMzf^EwR2>#$ASU2cm;Ga4^7jqZ?0H&n(E zs06$E*Gu22K2Rt010*2psh?AOaPntFZxzbQdhQy(dUlf$bGl(kKTy=lTvh>>TKVx7 z&!#lNO1V20EAatY#Hc3*r6T~y5D=D1A|J-W9o;xqn168BFRo)@V-sw>kJq9TtYkdh z1iC>Iu0A&Z$FD)pk#`uo80+_8mWzS?RhW;iI6ek*=peSLqj2pHUbWKcJl_5)^yQDH#Ca&th&-(uAm;hS!q zQ3l_m_UGIEH$Uo64gy03IXsxeO<-2hdI_6NceD`~+)o5Y00#J}hk_)VGzvK#u@2p8 zq6fLJy~&vjjhrlD=CP@joX>;V_*9M`w#A(;2i}p(`aVNtMa)3r>4c1nx)72BS$t3X zBRVD2{}X@~Q+DO|Q_}?)Dm8CN2%V3o z5nf3BC$FQrrx*INyWOApJ090DA&{q?jO5}EP|mN}5-27LNL{e`Mv1r6RZ!&@lB3X@TU;ki-4Dn?(`l{! zYtWIiSL5iR!PInMPUdw`x-2O=(kYp*yaZ6_yL}c%C0sT~;YcPK*4&;BFel+w z#-zY__p8bPCKlm@D8Zr|Zo_c8R7KUTH0e3S8w+sp@x;g1E2KUzuG3 zNPIE!W$2XBRf=3DE}6n2m8jgomAZ=`&1uuw7;@y=25Ol?x}2ZuPpWNWbH0fAJff-B zHjCL2rVGZmxS&~Ri3{wZiCYojYoy@s@V^P|%FU$^F_}Kg5O5V}f4&vGp6LlioFg&m zy+>MwX?7O_PjlTY>Y%ww4 zM8`!RF_+n*OA!+;$sHh6+PyMp2nDlaC@LzVM*M(v)LT)!x_6{ZZ4Lj$nyryf4fTiQ zD>=MhC>uTehJsnb75S#yLbM@k07)HM!`hRL0pkCNO2<_c{U^G579> z(8IMhp$9r0U|w@b;ojbJ=n4=CZZClS+=+i_ojpDp$R`*yExZdh_%t}kfXtkd{7$>y zB)JIhdJh%+N&4#mU{F}u&z`;45J3u!Q99x>dL}-K0M}r=3i`nRT}_}~{o`pj+4=P? zHTjydTSRL7qXb9Y!kfbl0EX))E1Tjp8}pgHD#1^D?yIe-bP%EihYuAB$=$1c6qWk0 zZDtnz0S^GWLuJ9MVu3Yz{m4i;+<;$uY(v`8)o#0l{)5I8*K{}G>Xmfx`a0WUiz{&z z4F}#`xNz)<3bF^wtjlJh74VX&H_KSvCzo1Pi2gxQyI1RyON?_C-A`D@*Cn>1jga(3DBw+2fI_M{+!)#=RI!jEpc%Bix0vBx!9&#TIEKi1; zk9{v0_#qveO@ShZe~~fm$L{eGRk`|fXn+kuXM38MGO+Az*xJq#2%hX?&L@VDS>=y+ zLa(U7ewe=(9mT&9QPTM@x3#F6LL?)?l?o~YR)TX?yRiK&V#x$$Ui9DwS=kbvB#ar} z34O=%H(G zBq^3QK?=SU4BA2;7?t0jjBK~Y%1YPGh~DBbyl!A!X?HgudEeM;@eD74&1DFqezj4X#BfA|`7*i56t= zcT_tx72nShG2!vK1v$r5OZ}l*4RC2aStOj2&s+Y$&?C69aJIW4VLfYSyhVlM0Z7W# z$Cc~ne@Z4CKwrv~HP>xVC{2xBzdJv>o9@NX=t#5jOp99`%-N?YQwcKa?}qrA3^<^~ z9gONiD)&5S7T36FzP;wZ43e?7bK=C8L%6;5M?}u_EQRyr$m;+C(+y zECfszcr5FsG=+DK*jwpbpq7{|*r2@+3?cF_ROci)J=P@a_o<6GhpY(M{WOVn)~|H9?_H2b-IUA|e6D%^kI0oWTHu8%@omS2GvBXChyrHRf-nX%+_@ z%*I^#N~vT{-Y);={Ts#6w_FP;)ZHBuv(m{vp^iHMXxFGnTOe^=urRVu6Sn+N8m~J5 z*1uNXz|wNGnio^=cz=IHg@NM|@G;j~^55TuC@YTfAtT3ZGQEYFxVnlQXiFpxI(%?` zTwDi1<%?k@7eOALs^+`OZsu=dkHt4I9c`MnOA`bC7g1-y7F7eS?Lks05$Ohz?(Pl& zY3UB>?k)-G?oR10X^@VgyKCsqp}zf|bFSOU0Uiv5hx#yhbz#YAn3*M49Wq@=*&7fEhHvjb zMG{D>9GqhQ{KyZKgCR)i|1>e7ar$vLA;f*#nWLXNDMDdBtSnw#X{eRSQXk8!&F7Ga zxGyI;K<3yD77F*g#V?CGd(%C{Jm3{1i&OS2an#>U6^>xer3yRmrrU$0QlO(V)z#Ed zX(Mt)i?Zx8|AJryFZ^}U=1eY!ActH5X5=Spx=%U?xJkCnO zlFF{uXbk9a0u7}@XExXt5BD+T){H$_Ns($k$3e&p*Kmd5G#(C~ax9**gJ#PbSxQ(D z4QP9=gMoo>Q(PM~^@NHm&*^%GQenH^8B=lE&Rxm5?C6SI97(2m(^oG9c$+>&n+VzO z0u8LKG5B0IeKfN;jJo4Ae<4W~xxPl|%@$@O-JJe1;YctSZukcM(O)*` zRCzK{+*karHGz}e_yZ0sWfB81mnl?EA#HBI9IKl~z46GBWM?(wuO^s2#TnO^S<8>- zF1s7+FMsCaN51e}9wP^IAZ`y|s^Pn>-pkl+H0Hn*B+kO9lAFVverD2M=G+w!9g3}tl(X1B@mR&q6r0fR6w!t4 z`u9x1*4v57t@ zl^Vcol%CD6$sqM-)F3pK;eFn=3uvr|3Pl`8HYqcp3a$tV~o; z`uBA9U^HZbkIxuap>Sm;G&qx;K|!#nBrNfePd6n@gTq}4?>${Hp);%7pNM!q`y4GW zWtnP%9%QntSz)k;d?psVVYxtGKG-3V`ie4 z)2%^=$Wdz&;H>Oj50r0k+Yqs}J6UvKC#Oq!J&xisU#MD>5lW$Fz+&H*0p@<=R-A6i z2Xu2eURY@!v;OcT6YJGD7{y^O+7U(;M^(nObUwPY`dvsyv$gS~*t?55RPh>E?}Xd- z@;at1Zvf6h&BLsm_N_`A1af`X8XuQX{^IjEe)jP6tgZU85eky#ek3R2%4SOa@#TOf z+(oSLE+#B;Zw%S~`8y`m$rGCEXga?60Q|k8ewaCQ^dj_(Z)_9*5O!vsE+njdHZt6rG*QwMQtJHcq}nG1+@e3@#6 zGjt}egn3eYysbzAPG9(9bH@;%ksW#ReKD^F0*ZP}w7db2ISmf7*&3?sDJLgH{q9e7 zJMS}bC$UY;U0PXmsI3CW+<+UaioYvQs6A5XIAYxVq;XA(TBY8garHtC77%-ifgDWc z^`T@NqmKysQbCDuHiF&-QMgy&U+||qevly^y6-Oiqv=H9`nfOoM9m(FAU=(9opK@! zEBz%So;Ox!_2u9fC);))Jf(?|QU1`+^hiyi*r0?^ZbbGc+?xQwAO%uc((A<=vJ?B@ zro06MTSGfhO(wichEsj}f$q+hYK9Jfhvh|HWmVP4gBkqd?~5+%uJ6aSCTP;7&WK^g z>Y;Z;^bwg*M07h_7hXpZMI1KgZ#!hF%zT4 zh>HGsokohcJ1_4Z;cdY7*_EFvnI0+Zd;y^WEvuBn}m&>PZ+4`+->57F|K zH~)*bJ9S?l0nlXm{|tLLK{6k$^G;IJ+^|Qo7`*n&qkKPOCuc6Kr=qi&lSJUC*F ze6!ZcS1gow(50Pie_7dh9f-SDRGw|DEi-DU_~ib2LqoSA;8}=!bEJNRd(J0qYpZvn zix%TfL7}2HLHmwjbUN%%vZZ^|9|;$iZI4aS7sCgeAf*uV+m9(+tk`culOV8Cy;HY; zK&B+Y2;OuHEPsl{a!uz2TFe3V_fpZz;}x#&)cOJ~O*qpx00+42Z1w{l3><9+{8^7A zy68b45K##Ts5m`yH!k8sS%oYd1xviev>d7Pqod^kuf_PGJWR!on{*O3@l=+;d7d1F z5$F)6*?Q^cq=yJTLtIt0g_9F2hg&ZAxS?S2*LwITUA21X$Av>a@7+IoWrJS)YO}TK zQmxO=qfEObxPvtFoeRYmOm_T!1BYTTnF`tbKVa<01~>V{Co?qv#&*=lD5g_QpYpkR z$p4(6Cr6g0TF3N5bJyFSjpuRx4DdKTdx%%pC2V%x#Mft^D9tGYo&bFLTU|{}(akH~ zNhFLgHV5&Rj(=;&%rC9qgPszDx8!_O`kZn{1jsp|SskP{P&f=Wq}gira#`@Y}xlO9U6qGCb#T5^Y3@_AcpJT@0Gv_X8` zPJVzLd0mP*noPF$VvE8(Y9uqK(~K%4EX;QKCU4sPE)v)wN}fMoI~^sZlifTbB6z^L zSNJV}oasfM%#YB8{ zr_lu=DNp3Z0qpb-1jJAkBQU_nPWO8k##ZO9pb_!aV?Z|$`QP$T_=OymVccMJzEYMb z;dCh?v?vAX015Nz^=&iIr+3`Jc>;d>Iz$)VJ=;mo0w8hFzI0Uda`=MnS_uQ+y^5K( zf|l0b=oME-)cnXE{EK!)_-rQ3jfHZ%UJ+QpN~jpK+nG2^7j>_NvmqQ!E9rBk)hJXH zg_7;}t?DeBogQd5T*2et@Y$pW0QdappV9vT?nU}7%_oaeMBm3O&YmScJcMiQ1F)CH z-KvE1`Brnf=e(ydHxyNIu8%IP=jt&c`F*yjZ25vs1%B67mLrU4g{}^H#S6OUOw31X4 zy~3NL4q}Su$$Iu(Kf*f=*VPjcaeK?a%sCkA&i`%{KQf1_xRLv&z6{Io>(}7aKxJD} z6DYf_<#DEhhf;>%sMBV}#a|AN!TAz-rzyU{hVlzc1&R_O4Ri5zevIMTHnr0~Tw*6v z|CR~fuFY0p9Rx(pRNXP%O}xF^Hz`Va@g%!tarw{FDu&WSde%o#vMDHA$PBEsUga#O z^5I84;#0&JuWym}@jF;EVhugO7s0PzkULYBOVID@$+L@x%~gZ$&V@A4>?KVok31<7 z61x0AQTR!+6q1qoKnyA&xDh9(gBcn_m_#+vN{B!kON?8j0V1YgXe} zR?MO#jSacqjRJ4e=F2X{VkljF@a-UqnLiLdtASP9Xb1?8oTsA%(1Koz(fL!XK*%xb zEWeN&@zkV}5z&)kc&xhD{n;RzBT|FtY#kh3tzTRo{B09#P{Gj5O6cFgk|}^JAGsxn zE(IEG)$+qlT)nH&vt=j$C~R!aGSr1OWj~)|jnLPm#bhXw-V_Fp*Fax_81w-D2Kg6XMHK z1iwP1nO4M2;~-&w<#C?grIA9zT|v=QB{zE~pZHwQluf;>R;K$vh?Q%A@Ni;(AE7%1hC(?Yjns;H#k~vN2Xwh{elF6$hc_p*i zVg}Z;wwi>vu{!;mujJFjj_k!-q)Z)cAq}df4{{CaxmI*N`!RX}tkFyqZ+J*aH4Y{O zxOue~40ZQDGPwIjDWpemX#Ln~-_{u(&mg@q;tG#Y_z4xoCLMa5VtH>gUWbLK5{adm z8y~OIpGLssuA)BS9uNjKqa`J9d@cNz5X_$=j1hMU#G1(Vy-0)3E!$9RBe?`w(gvb( zR2k{eItMk7kt3+TewDu%gmZLs&D-78t00ik)IUoql1*WA_q{o|blmCJ$<6#cGTU=U zb{{Vp^V`G>0k|*znMPbhkTi|KYNrglZ?5o|*la!M@)mkPWKG>u$mxGW%&Atd<_MPf zXFh>+x;pUhE}vWC)v6x^;&A;YE~1(jlrx!33F~faX-RuCRxabmvS3Txx!pdx>_faMBy&aTZ!$DV-vg!!XLC)QH05k?w5Jo_xxtAjv^X4XJ z{e6IJ)W82t26|(dw$RD_Ej08aiB-d z(UZ9+f#LU4V}w5c=y7^aO_5G0aSkDc55lEiz{VLl4f(z@)Kb9lJ&iL16-~Q;AMsky zZ3`J1)iL=xS$vn)XCc`2wnfz=$E0%qhdZ4H_Xs^OEuSG13lV+``hZY_6OkvF$?p