From fd4b8e339a47ed2532f9c95a7927c65b1fa75e0d Mon Sep 17 00:00:00 2001 From: Katerina Patticha Date: Thu, 9 Jun 2022 22:19:32 +0200 Subject: [PATCH 01/62] [APM] Skip integration page test (#134100) --- .../power_user/integration_settings/integration_policy.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/integration_settings/integration_policy.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/integration_settings/integration_policy.spec.ts index f87d9c47e6624..655376eb3eb63 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/integration_settings/integration_policy.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/integration_settings/integration_policy.spec.ts @@ -52,7 +52,7 @@ const apisToIntercept = [ }, ]; -describe('when navigating to integration page', () => { +describe.skip('when navigating to integration page', () => { beforeEach(() => { const integrationsPath = '/app/integrations/browse'; From b75d96491fd17841d13dc49b1736c691d77714e5 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Thu, 9 Jun 2022 16:26:58 -0400 Subject: [PATCH 02/62] [ResponseOps] Visualize alerting metrics in Stack Monitoring (#123726) * Add new plugint to collect additional kibana monitoring metrics * Readme * Update generated document * WIP * Remove task manager and add support for max number * Use MAX_SAFE_INTEGER * We won't use this route * Tests and lint * Track actions * Use dynamic route style * Initial attempt * Fix test * Fix some tests * Couple small fixes * Add in mapping verification * Adapt to new changes in base PR * Fix types * Feedback from PR * PR feedback * We do not need this * PR feedback * Match options to api/stats * Remove internal collection support * Fix api change * Fix small issues * Separate cluster and node metrics * Add more tests * Add retryAt in the test too * Add logging and use a class * fix types * Fix tests * Fix bad merge * Separate these two out * Update for new fields * PR feedback * Update terminology and add timeouts * Add types * Fix types * Fix types and tests * Fix tests * Linting fixes * Use MB fields directly * Fix tests * Fix snapshot * Do not test for mappings for metricbeat-only metrics * Fix tests * PR feedback * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Add actions * Fix tests * Fix lint * Hide from internal monitoring * Add tests * Fix tests * PR feedback * Remove mappings * More defensive * Skip for now until other work is done * Update x-pack/plugins/monitoring/server/lib/metrics/kibana/metrics.ts * update unit and fix related tests * update test snapshot related to changes in unit * unskip api integration tests and update for unit change * update fixtures from merge * fix description of graphs Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Mat Schaffer Co-authored-by: neptunian --- .../server/rule_type_registry.test.ts | 1 - .../application/pages/kibana/instance.tsx | 41 ++ .../application/pages/kibana/overview.tsx | 41 ++ .../cluster/overview/kibana_panel.js | 27 +- .../server/kibana_monitoring/bulk_uploader.ts | 1 + .../lib/cluster/get_clusters_from_request.ts | 30 +- .../server/lib/details/get_series.ts | 44 +- .../get_cluster_rule_data_for_clusters.ts | 93 +++ .../get_instance_rule_data_for_clusters.ts | 90 +++ .../server/lib/kibana/rules/index.ts | 8 + .../__snapshots__/metrics.test.js.snap | 460 ++++++++++++ .../server/lib/metrics/classes/metric.ts | 2 + .../monitoring/server/lib/metrics/index.ts | 7 +- .../server/lib/metrics/kibana/classes.ts | 87 ++- .../server/lib/metrics/kibana/metrics.ts | 178 ++++- .../api/v1/kibana/metric_set_instance.ts | 4 + .../api/v1/kibana/metric_set_overview.ts | 10 + .../server/routes/api/v1/kibana/overview.ts | 23 +- .../cluster/fixtures/multicluster.json | 204 ++---- .../monitoring/cluster/fixtures/overview.json | 3 +- .../apis/monitoring/common/mappings_exist.js | 1 + .../monitoring/kibana/fixtures/instance.json | 666 ++++++------------ .../monitoring/kibana/fixtures/overview.json | 370 ++++++---- .../apis/monitoring/kibana/index.js | 2 + .../apis/monitoring/kibana/instance.js | 1 - .../apis/monitoring/kibana/instance_mb.js | 2 + .../apis/monitoring/kibana/overview.js | 1 - .../apis/monitoring/kibana/overview_mb.js | 2 + .../rules_and_actions/fixtures/instance.json | 334 +++++++++ .../rules_and_actions/fixtures/overview.json | 81 +++ .../kibana/rules_and_actions/index.js | 13 + .../kibana/rules_and_actions/instance.js | 42 ++ .../kibana/rules_and_actions/overview.js | 40 ++ .../normalize_data_type_differences.ts | 15 +- .../apis/monitoring/set_indices_found.tsx | 25 + .../standalone_cluster/fixtures/clusters.json | 4 +- .../kibana/rules_and_actions/data.json.gz | Bin 0 -> 20462 bytes 37 files changed, 2171 insertions(+), 782 deletions(-) create mode 100644 x-pack/plugins/monitoring/server/lib/kibana/rules/get_cluster_rule_data_for_clusters.ts create mode 100644 x-pack/plugins/monitoring/server/lib/kibana/rules/get_instance_rule_data_for_clusters.ts create mode 100644 x-pack/plugins/monitoring/server/lib/kibana/rules/index.ts create mode 100644 x-pack/test/api_integration/apis/monitoring/kibana/rules_and_actions/fixtures/instance.json create mode 100644 x-pack/test/api_integration/apis/monitoring/kibana/rules_and_actions/fixtures/overview.json create mode 100644 x-pack/test/api_integration/apis/monitoring/kibana/rules_and_actions/index.js create mode 100644 x-pack/test/api_integration/apis/monitoring/kibana/rules_and_actions/instance.js create mode 100644 x-pack/test/api_integration/apis/monitoring/kibana/rules_and_actions/overview.js create mode 100644 x-pack/test/api_integration/apis/monitoring/set_indices_found.tsx create mode 100644 x-pack/test/functional/es_archives/monitoring/kibana/rules_and_actions/data.json.gz diff --git a/x-pack/plugins/alerting/server/rule_type_registry.test.ts b/x-pack/plugins/alerting/server/rule_type_registry.test.ts index 9a6b2232c47d4..ed52ebf8b04da 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.test.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.test.ts @@ -20,7 +20,6 @@ let mockedLicenseState: jest.Mocked; let ruleTypeRegistryParams: ConstructorOptions; const taskManager = taskManagerMock.createSetup(); - const inMemoryMetrics = inMemoryMetricsMock.create(); beforeEach(() => { diff --git a/x-pack/plugins/monitoring/public/application/pages/kibana/instance.tsx b/x-pack/plugins/monitoring/public/application/pages/kibana/instance.tsx index 5332968c1c34a..a433ac23de4e0 100644 --- a/x-pack/plugins/monitoring/public/application/pages/kibana/instance.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/kibana/instance.tsx @@ -37,6 +37,11 @@ import { RULE_KIBANA_VERSION_MISMATCH } from '../../../../common/constants'; const KibanaInstance = ({ data, alerts }: { data: any; alerts: any }) => { const { zoomInfo, onBrush } = useCharts(); + const showRules = + data.metrics.kibana_instance_rule_executions && + data.metrics.kibana_instance_rule_executions.length && + data.metrics.kibana_instance_rule_executions[0].indices_found.metricbeat; + return ( @@ -95,6 +100,42 @@ const KibanaInstance = ({ data, alerts }: { data: any; alerts: any }) => { /> + {showRules && ( + <> + + + + + + + + + + + + + + + + + + )} diff --git a/x-pack/plugins/monitoring/public/application/pages/kibana/overview.tsx b/x-pack/plugins/monitoring/public/application/pages/kibana/overview.tsx index 078559ac30663..834fa396fb44f 100644 --- a/x-pack/plugins/monitoring/public/application/pages/kibana/overview.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/kibana/overview.tsx @@ -33,6 +33,11 @@ const KibanaOverview = ({ data }: { data: any }) => { if (!data) return null; + const showRules = + data.metrics.kibana_cluster_rule_overdue_count && + data.metrics.kibana_cluster_rule_overdue_count.length && + data.metrics.kibana_cluster_rule_overdue_count[0].indices_found.metricbeat; + return ( @@ -57,6 +62,42 @@ const KibanaOverview = ({ data }: { data: any }) => { /> + {showRules && ( + <> + + + + + + + + + + + + + + + + + + )} diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js index 086303993106b..160818aa6babd 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js @@ -28,7 +28,7 @@ import { SetupModeFeature } from '../../../../common/enums'; import { AlertsBadge } from '../../../alerts/badge'; import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; import { ExternalConfigContext } from '../../../application/contexts/external_config_context'; -import { formatNumber } from '../../../lib/format_number'; +import { formatNumber, formatPercentageUsage } from '../../../lib/format_number'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; import { SetupModeContext } from '../../setup_mode/setup_mode_context'; @@ -143,6 +143,31 @@ export function KibanaPanel(props) { values={{ maxTime: props.response_time_max }} /> + {props.rules.instance && props.rules.cluster && ( + <> + + + + + {formatPercentageUsage( + props.rules.instance.executions - props.rules.instance.failures, + props.rules.instance.executions + )} + + + + + + {props.rules.cluster.overdue.count} + + + )} diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.ts index 83f5d2037bd70..8abbe4fea1f8e 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.ts @@ -70,6 +70,7 @@ export class BulkUploader implements IBulkUploader { private _timer: NodeJS.Timer | null; private readonly _interval: number; private readonly config: MonitoringConfig; + constructor({ log, config, diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.ts b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.ts index 86de6f6c79623..d8bf8487cee09 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.ts @@ -6,7 +6,7 @@ */ import { notFound } from '@hapi/boom'; -import { get } from 'lodash'; +import { get, omit } from 'lodash'; import { set } from '@elastic/safer-lodash-set'; import { i18n } from '@kbn/i18n'; import { getClustersStats } from './get_clusters_stats'; @@ -39,6 +39,7 @@ import { getLogTypes } from '../logs'; import { isInCodePath } from './is_in_code_path'; import { LegacyRequest, Cluster } from '../../types'; import { RulesByType } from '../../../common/types/alerts'; +import { getClusterRuleDataForClusters, getInstanceRuleDataForClusters } from '../kibana/rules'; /** * Get all clusters or the cluster associated with {@code clusterUuid} when it is defined. @@ -168,10 +169,14 @@ export async function getClustersFromRequest( } } // add kibana data - const kibanas = + const [kibanas, kibanaClusterRules, kibanaInstanceRules] = isInCodePath(codePaths, [CODE_PATH_KIBANA]) && !isStandaloneCluster - ? await getKibanasForClusters(req, clusters, CCS_REMOTE_PATTERN) - : []; + ? await Promise.all([ + getKibanasForClusters(req, clusters, CCS_REMOTE_PATTERN), + getClusterRuleDataForClusters(req, clusters, CCS_REMOTE_PATTERN), + getInstanceRuleDataForClusters(req, clusters, CCS_REMOTE_PATTERN), + ]) + : [[], [], []]; // add the kibana data to each cluster kibanas.forEach((kibana) => { const clusterIndex = clusters.findIndex( @@ -179,6 +184,23 @@ export async function getClustersFromRequest( get(cluster, 'elasticsearch.cluster.id', cluster.cluster_uuid) === kibana.clusterUuid ); set(clusters[clusterIndex], 'kibana', kibana.stats); + + const clusterKibanaRules = kibanaClusterRules.every((rule) => !Boolean(rule)) + ? null + : kibanaClusterRules?.find((rule) => rule?.clusterUuid === kibana.clusterUuid); + const instanceKibanaRules = kibanaInstanceRules.every((rule) => !Boolean(rule)) + ? null + : kibanaInstanceRules?.find((rule) => rule?.clusterUuid === kibana.clusterUuid); + set( + clusters[clusterIndex], + 'kibana.rules.cluster', + clusterKibanaRules ? omit(clusterKibanaRules, 'clusterUuid') : null + ); + set( + clusters[clusterIndex], + 'kibana.rules.instance', + instanceKibanaRules ? omit(instanceKibanaRules, 'clusterUuid') : null + ); }); // add logstash data diff --git a/x-pack/plugins/monitoring/server/lib/details/get_series.ts b/x-pack/plugins/monitoring/server/lib/details/get_series.ts index 58e1aac0884b6..495c1a7642d6d 100644 --- a/x-pack/plugins/monitoring/server/lib/details/get_series.ts +++ b/x-pack/plugins/monitoring/server/lib/details/get_series.ts @@ -18,13 +18,16 @@ import { CALCULATE_DURATION_UNTIL, INDEX_PATTERN_TYPES, STANDALONE_CLUSTER_CLUSTER_UUID, + METRICBEAT_INDEX_NAME_UNIQUE_TOKEN, } from '../../../common/constants'; import { formatUTCTimestampForTimezone } from '../format_timezone'; import { getNewIndexPatterns } from '../cluster/get_index_patterns'; import { Globals } from '../../static_globals'; import type { Metric } from '../metrics/metrics'; -type SeriesBucket = Bucket & { metric_mb_deriv?: { normalized_value: number } }; +type SeriesBucket = Bucket & { metric_mb_deriv?: { normalized_value: number } } & { + indices?: { buckets: Array<{ [key: string]: any }> }; +}; /** * Derivative metrics for the first two agg buckets are unusable. For the first bucket, there @@ -152,6 +155,11 @@ async function fetchSeries( }, aggs: { ...dateHistogramSubAggs, + indices: { + terms: { + field: '_index', + }, + }, }, }, }; @@ -267,7 +275,7 @@ function handleSeries( timezone: string, response: ElasticsearchResponse ) { - const { derivative, calculation: customCalculation } = metric; + const { derivative, calculation: customCalculation, isNotSupportedInInternalCollection } = metric; function getAggregatedData(buckets: SeriesBucket[]) { const firstUsableBucketIndex = findFirstUsableBucketIndex(buckets, min); @@ -277,21 +285,44 @@ function handleSeries( firstUsableBucketIndex, bucketSizeInSeconds * 1000 ); + let internalIndicesFound = false; + let mbIndicesFound = false; let data: Array<[string | number, number | null]> = []; if (firstUsableBucketIndex <= lastUsableBucketIndex) { // map buckets to values for charts const key = derivative ? 'metric_deriv.normalized_value' : 'metric.value'; const calculation = customCalculation !== undefined ? customCalculation : defaultCalculation; + const usableBuckets = buckets.slice(firstUsableBucketIndex, lastUsableBucketIndex + 1); // take only the buckets we know are usable + + data = usableBuckets.map((bucket) => { + // map buckets to X/Y coords for Flot charting + if (bucket.indices) { + for (const indexBucket of bucket.indices.buckets) { + if (indexBucket.key.includes(METRICBEAT_INDEX_NAME_UNIQUE_TOKEN)) { + mbIndicesFound = true; + } else { + internalIndicesFound = true; + } + } + } - data = buckets - .slice(firstUsableBucketIndex, lastUsableBucketIndex + 1) // take only the buckets we know are usable - .map((bucket) => [ + return [ formatUTCTimestampForTimezone(bucket.key, timezone), calculation(bucket, key, metric, bucketSizeInSeconds), - ]); // map buckets to X/Y coords for Flot charting + ]; + }); } + const indexSourceData = isNotSupportedInInternalCollection + ? { + indices_found: { + internal: internalIndicesFound, + metricbeat: mbIndicesFound, + }, + } + : {}; + return { bucket_size: formatBucketSize(bucketSizeInSeconds), timeRange: { @@ -299,6 +330,7 @@ function handleSeries( max: formatUTCTimestampForTimezone(max, timezone), }, metric: metric.serialize(), + ...indexSourceData, data, }; } diff --git a/x-pack/plugins/monitoring/server/lib/kibana/rules/get_cluster_rule_data_for_clusters.ts b/x-pack/plugins/monitoring/server/lib/kibana/rules/get_cluster_rule_data_for_clusters.ts new file mode 100644 index 0000000000000..2a2b39259beeb --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/kibana/rules/get_cluster_rule_data_for_clusters.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { Cluster, LegacyRequest } from '../../../types'; +import { getNewIndexPatterns } from '../../cluster/get_index_patterns'; +import { Globals } from '../../../static_globals'; +import { createQuery } from '../../create_query'; +import { KibanaClusterRuleMetric } from '../../metrics'; + +export async function getClusterRuleDataForClusters( + req: LegacyRequest, + clusters: Cluster[], + ccs: string +) { + const start = req.payload.timeRange.min; + const end = req.payload.timeRange.max; + + const moduleType = 'kibana'; + const type = 'kibana_cluster_rules'; + const dataset = 'cluster_rules'; + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + moduleType, + dataset, + ccs, + }); + + return Promise.all( + clusters.map(async (cluster) => { + const clusterUuid = cluster.elasticsearch?.cluster?.id ?? cluster.cluster_uuid; + const metric = KibanaClusterRuleMetric.getMetricFields(); + const params = { + index: indexPatterns, + size: 0, + ignore_unavailable: true, + body: { + query: createQuery({ + type, + dsDataset: `${moduleType}.${dataset}`, + metricset: dataset, + start, + end, + clusterUuid, + metric, + }), + aggs: { + indices: { + terms: { + field: '_index', + size: 1, + }, + }, + overdue_count: { + max: { + field: 'kibana.cluster_rules.overdue.count', + }, + }, + overdue_delay_p50: { + max: { + field: 'kibana.cluster_rules.overdue.delay.p50', + }, + }, + overdue_delay_p99: { + max: { + field: 'kibana.cluster_rules.overdue.delay.p99', + }, + }, + }, + }, + }; + const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); + const response = await callWithRequest(req, 'search', params); + const indices = response.aggregations?.indices?.buckets ?? []; + if (indices.length === 0) { + // This means they are only using internal monitoring and rule monitoring data is not available + return null; + } + return { + overdue: { + count: response.aggregations?.overdue_count?.value, + delay: { + p50: response.aggregations?.overdue_delay_p50?.value, + p99: response.aggregations?.overdue_delay_p99?.value, + }, + }, + clusterUuid, + }; + }) + ); +} diff --git a/x-pack/plugins/monitoring/server/lib/kibana/rules/get_instance_rule_data_for_clusters.ts b/x-pack/plugins/monitoring/server/lib/kibana/rules/get_instance_rule_data_for_clusters.ts new file mode 100644 index 0000000000000..c2c420952151e --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/kibana/rules/get_instance_rule_data_for_clusters.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { Cluster, LegacyRequest } from '../../../types'; +import { getNewIndexPatterns } from '../../cluster/get_index_patterns'; +import { Globals } from '../../../static_globals'; +import { createQuery } from '../../create_query'; +import { KibanaClusterRuleMetric } from '../../metrics'; + +export async function getInstanceRuleDataForClusters( + req: LegacyRequest, + clusters: Cluster[], + ccs: string +) { + const start = req.payload.timeRange.min; + const end = req.payload.timeRange.max; + + const moduleType = 'kibana'; + const type = 'kibana_node_rules'; + const dataset = 'node_rules'; + const indexPatterns = getNewIndexPatterns({ + config: Globals.app.config, + moduleType, + dataset, + ccs, + }); + + return Promise.all( + clusters.map(async (cluster) => { + const clusterUuid = cluster.elasticsearch?.cluster?.id ?? cluster.cluster_uuid; + const metric = KibanaClusterRuleMetric.getMetricFields(); + const params = { + index: indexPatterns, + size: 0, + ignore_unavailable: true, + body: { + query: createQuery({ + type, + dsDataset: `${moduleType}.${dataset}`, + metricset: dataset, + start, + end, + clusterUuid, + metric, + }), + aggs: { + indices: { + terms: { + field: '_index', + size: 1, + }, + }, + executions: { + max: { + field: 'kibana.node_rules.executions', + }, + }, + failures: { + max: { + field: 'kibana.node_rules.failures', + }, + }, + timeouts: { + max: { + field: 'kibana.node_rules.timeouts', + }, + }, + }, + }, + }; + + const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); + const response = await callWithRequest(req, 'search', params); + const indices = response.aggregations?.indices?.buckets ?? []; + if (indices.length === 0) { + // This means they are only using internal monitoring and rule monitoring data is not available + return null; + } + return { + failures: response.aggregations?.failures?.value, + executions: response.aggregations?.executions?.value, + timeouts: response.aggregations?.timeouts?.value, + clusterUuid, + }; + }) + ); +} diff --git a/x-pack/plugins/monitoring/server/lib/kibana/rules/index.ts b/x-pack/plugins/monitoring/server/lib/kibana/rules/index.ts new file mode 100644 index 0000000000000..3ab38f03dd200 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/kibana/rules/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export { getClusterRuleDataForClusters } from './get_cluster_rule_data_for_clusters'; +export { getInstanceRuleDataForClusters } from './get_instance_rule_data_for_clusters'; diff --git a/x-pack/plugins/monitoring/server/lib/metrics/__snapshots__/metrics.test.js.snap b/x-pack/plugins/monitoring/server/lib/metrics/__snapshots__/metrics.test.js.snap index 50fff3734b1a5..91f4779509af2 100644 --- a/x-pack/plugins/monitoring/server/lib/metrics/__snapshots__/metrics.test.js.snap +++ b/x-pack/plugins/monitoring/server/lib/metrics/__snapshots__/metrics.test.js.snap @@ -41,6 +41,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Count", "mbField": undefined, "metricAgg": "max", @@ -91,6 +92,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Count", "mbField": undefined, "metricAgg": "max", @@ -141,6 +143,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Error Count", "mbField": undefined, "metricAgg": "max", @@ -191,6 +194,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Count", "mbField": undefined, "metricAgg": "max", @@ -241,6 +245,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Invalid Query", "mbField": undefined, "metricAgg": "max", @@ -291,6 +296,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Method", "mbField": undefined, "metricAgg": "max", @@ -341,6 +347,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Unauthorized", "mbField": undefined, "metricAgg": "max", @@ -391,6 +398,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Unavailable", "mbField": undefined, "metricAgg": "max", @@ -441,6 +449,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Not Modified", "mbField": undefined, "metricAgg": "max", @@ -491,6 +500,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "OK", "mbField": undefined, "metricAgg": "max", @@ -544,6 +554,7 @@ Object { "fieldSource": "beats_stats.metrics.beat.cgroup", "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Cgroup CPU Utilization", "mbField": undefined, "metricAgg": "max", @@ -567,6 +578,7 @@ Object { "fieldSource": undefined, "format": "0,0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Memory Limit", "mbField": undefined, "metricAgg": "max", @@ -590,6 +602,7 @@ Object { "fieldSource": undefined, "format": "0,0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Memory Utilization (cgroup)", "mbField": undefined, "metricAgg": "max", @@ -613,6 +626,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Total", "mbField": undefined, "metricAgg": "max", @@ -636,6 +650,7 @@ Object { "fieldSource": undefined, "format": "0,0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Allocated Memory", "mbField": undefined, "metricAgg": "max", @@ -659,6 +674,7 @@ Object { "fieldSource": undefined, "format": "0,0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "GC Next", "mbField": undefined, "metricAgg": "max", @@ -682,6 +698,7 @@ Object { "fieldSource": undefined, "format": "0,0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Process Total", "mbField": undefined, "metricAgg": "max", @@ -732,6 +749,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Acked", "mbField": undefined, "metricAgg": "max", @@ -782,6 +800,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Active", "mbField": undefined, "metricAgg": "max", @@ -832,6 +851,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Dropped", "mbField": undefined, "metricAgg": "max", @@ -882,6 +902,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Failed", "mbField": undefined, "metricAgg": "max", @@ -932,6 +953,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Total", "mbField": undefined, "metricAgg": "max", @@ -982,6 +1004,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Error", "mbField": undefined, "metricAgg": "max", @@ -1032,6 +1055,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Metric", "mbField": undefined, "metricAgg": "max", @@ -1082,6 +1106,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Span", "mbField": undefined, "metricAgg": "max", @@ -1132,6 +1157,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Transaction", "mbField": undefined, "metricAgg": "max", @@ -1182,6 +1208,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Requested", "mbField": undefined, "metricAgg": "max", @@ -1232,6 +1259,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Total", "mbField": undefined, "metricAgg": "max", @@ -1282,6 +1310,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Closed", "mbField": undefined, "metricAgg": "max", @@ -1332,6 +1361,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Concurrency", "mbField": undefined, "metricAgg": "max", @@ -1382,6 +1412,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Decode", "mbField": undefined, "metricAgg": "max", @@ -1432,6 +1463,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Forbidden", "mbField": undefined, "metricAgg": "max", @@ -1482,6 +1514,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Internal", "mbField": undefined, "metricAgg": "max", @@ -1532,6 +1565,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Method", "mbField": undefined, "metricAgg": "max", @@ -1582,6 +1616,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Queue", "mbField": undefined, "metricAgg": "max", @@ -1632,6 +1667,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Rate limit", "mbField": undefined, "metricAgg": "max", @@ -1682,6 +1718,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Too large", "mbField": undefined, "metricAgg": "max", @@ -1732,6 +1769,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Unauthorized", "mbField": undefined, "metricAgg": "max", @@ -1782,6 +1820,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Validate", "mbField": undefined, "metricAgg": "max", @@ -1832,6 +1871,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Accepted", "mbField": undefined, "metricAgg": "max", @@ -1882,6 +1922,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Ok", "mbField": undefined, "metricAgg": "max", @@ -1905,6 +1946,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "1m", "mbField": undefined, "metricAgg": "max", @@ -1928,6 +1970,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "15m", "mbField": undefined, "metricAgg": "max", @@ -1951,6 +1994,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "5m", "mbField": undefined, "metricAgg": "max", @@ -1974,6 +2018,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "App Search Engines", "mbField": undefined, "metricAgg": "avg", @@ -1996,6 +2041,7 @@ Object { "fieldSource": undefined, "format": "0,0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Bytes Sent", "mbField": undefined, "metricAgg": "max", @@ -2046,6 +2092,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Acknowledged", "mbField": undefined, "metricAgg": "max", @@ -2096,6 +2143,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Dropped in Output", "mbField": undefined, "metricAgg": "max", @@ -2146,6 +2194,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Emitted", "mbField": undefined, "metricAgg": "max", @@ -2196,6 +2245,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Bytes Received", "mbField": undefined, "metricAgg": "max", @@ -2246,6 +2296,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Receiving", "mbField": undefined, "metricAgg": "max", @@ -2296,6 +2347,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Sending", "mbField": undefined, "metricAgg": "max", @@ -2346,6 +2398,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Bytes Sent", "mbField": undefined, "metricAgg": "max", @@ -2396,6 +2449,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Dropped in Pipeline", "mbField": undefined, "metricAgg": "max", @@ -2446,6 +2500,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Queued", "mbField": undefined, "metricAgg": "max", @@ -2496,6 +2551,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Failed in Pipeline", "mbField": undefined, "metricAgg": "max", @@ -2546,6 +2602,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Retry in Pipeline", "mbField": undefined, "metricAgg": "max", @@ -2596,6 +2653,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Total", "mbField": undefined, "metricAgg": "max", @@ -2619,6 +2677,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Total", "mbField": undefined, "metricAgg": "max", @@ -2642,6 +2701,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Open Handles", "mbField": undefined, "metricAgg": "max", @@ -2665,6 +2725,7 @@ Object { "fieldSource": undefined, "format": "0,0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Active", "mbField": undefined, "metricAgg": "max", @@ -2688,6 +2749,7 @@ Object { "fieldSource": undefined, "format": "0,0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "GC Next", "mbField": undefined, "metricAgg": "max", @@ -2711,6 +2773,7 @@ Object { "fieldSource": undefined, "format": "0,0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Process Total", "mbField": undefined, "metricAgg": "max", @@ -2734,6 +2797,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Acknowledged", "mbField": undefined, "metricAgg": "max", @@ -2757,6 +2821,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Dropped in Output", "mbField": undefined, "metricAgg": "max", @@ -2780,6 +2845,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Emitted", "mbField": undefined, "metricAgg": "max", @@ -2803,6 +2869,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Receiving", "mbField": undefined, "metricAgg": "max", @@ -2826,6 +2893,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Sending", "mbField": undefined, "metricAgg": "max", @@ -2849,6 +2917,7 @@ Object { "fieldSource": undefined, "format": "0,0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Bytes Received", "mbField": undefined, "metricAgg": "max", @@ -2872,6 +2941,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Dropped in Pipeline", "mbField": undefined, "metricAgg": "max", @@ -2895,6 +2965,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Queued", "mbField": undefined, "metricAgg": "max", @@ -2918,6 +2989,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Failed in Pipeline", "mbField": undefined, "metricAgg": "max", @@ -2941,6 +3013,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Retry in Pipeline", "mbField": undefined, "metricAgg": "max", @@ -2964,6 +3037,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "New", "mbField": undefined, "metricAgg": "max", @@ -2987,6 +3061,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "1m", "mbField": undefined, "metricAgg": "max", @@ -3010,6 +3085,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "15m", "mbField": undefined, "metricAgg": "max", @@ -3033,6 +3109,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "5m", "mbField": undefined, "metricAgg": "max", @@ -3068,6 +3145,7 @@ Object { "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, "getFields": [Function], + "isNotSupportedInInternalCollection": undefined, "label": "Ops delay", "mbField": undefined, "metricAgg": "sum", @@ -3092,6 +3170,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Fetch delay", "mbField": undefined, "metricAgg": "max", @@ -3141,6 +3220,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Indexing Latency", "mbField": undefined, "metricAgg": "sum", @@ -3164,6 +3244,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Primary Shards", "mbField": undefined, "metricAgg": "max", @@ -3188,6 +3269,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Total Shards", "mbField": undefined, "metricAgg": "max", @@ -3237,6 +3319,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Search Latency", "mbField": undefined, "metricAgg": "sum", @@ -3260,6 +3343,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Total Shards", "mbField": undefined, "metricAgg": "max", @@ -3284,6 +3368,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Active", "mbField": undefined, "metricAgg": "max", @@ -3306,6 +3391,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Total", "mbField": undefined, "metricAgg": "max", @@ -3329,6 +3415,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Daemon Threads", "mbField": undefined, "metricAgg": "max", @@ -3351,6 +3438,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "JVM GC Rate", "mbField": undefined, "metricAgg": "max", @@ -3373,6 +3461,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Time spent on JVM garbage collection", "mbField": undefined, "metricAgg": "max", @@ -3395,6 +3484,7 @@ Object { "fieldSource": undefined, "format": "0,0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Committed", "mbField": undefined, "metricAgg": "max", @@ -3417,6 +3507,7 @@ Object { "fieldSource": undefined, "format": "0,0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Total", "mbField": undefined, "metricAgg": "max", @@ -3440,6 +3531,7 @@ Object { "fieldSource": undefined, "format": "0,0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Used", "mbField": undefined, "metricAgg": "max", @@ -3462,6 +3554,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "1xx", "mbField": undefined, "metricAgg": "max", @@ -3485,6 +3578,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "2xx", "mbField": undefined, "metricAgg": "max", @@ -3507,6 +3601,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "3xx", "mbField": undefined, "metricAgg": "max", @@ -3529,6 +3624,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "4xx", "mbField": undefined, "metricAgg": "max", @@ -3551,6 +3647,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "5xx", "mbField": undefined, "metricAgg": "max", @@ -3573,6 +3670,7 @@ Object { "fieldSource": undefined, "format": "0,0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Received", "mbField": undefined, "metricAgg": "max", @@ -3596,6 +3694,7 @@ Object { "fieldSource": undefined, "format": "0,0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "HTTP Bytes Received", "mbField": undefined, "metricAgg": "max", @@ -3618,6 +3717,7 @@ Object { "fieldSource": undefined, "format": "0,0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Sent", "mbField": undefined, "metricAgg": "max", @@ -3640,6 +3740,7 @@ Object { "fieldSource": undefined, "format": "0,0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "HTTP Bytes Sent", "mbField": undefined, "metricAgg": "max", @@ -3662,6 +3763,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Open HTTP Connections", "mbField": undefined, "metricAgg": "max", @@ -3684,6 +3786,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "HTTP Connections Rate", "mbField": undefined, "metricAgg": "max", @@ -3706,6 +3809,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "JVM Objects Pending Finalization", "mbField": undefined, "metricAgg": "max", @@ -3728,6 +3832,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Active Threads", "mbField": undefined, "metricAgg": "max", @@ -3751,6 +3856,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Thread Creation Rate", "mbField": undefined, "metricAgg": "max", @@ -3773,6 +3879,7 @@ Object { "fieldSource": undefined, "format": "0,0.[0]a", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Document Count", "mbField": undefined, "metricAgg": "max", @@ -3821,6 +3928,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Indexing Latency", "mbField": undefined, "metricAgg": "sum", @@ -3845,6 +3953,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Indexing (Primaries)", "mbField": undefined, "metricAgg": "max", @@ -3869,6 +3978,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Index Total", "mbField": undefined, "metricAgg": "max", @@ -3893,6 +4003,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Indexing", "mbField": undefined, "metricAgg": "max", @@ -3917,6 +4028,7 @@ Object { "fieldSource": undefined, "format": "0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Fielddata", "mbField": undefined, "metricAgg": "max", @@ -3941,6 +4053,7 @@ Object { "fieldSource": undefined, "format": "0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Fixed Bitsets", "mbField": undefined, "metricAgg": "max", @@ -3965,6 +4078,7 @@ Object { "fieldSource": undefined, "format": "0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Query Cache", "mbField": undefined, "metricAgg": "max", @@ -3989,6 +4103,7 @@ Object { "fieldSource": undefined, "format": "0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Request Cache", "mbField": undefined, "metricAgg": "max", @@ -4013,6 +4128,7 @@ Object { "fieldSource": undefined, "format": "0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Version Map", "mbField": undefined, "metricAgg": "max", @@ -4037,6 +4153,7 @@ Object { "fieldSource": undefined, "format": "0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Index Writer", "mbField": undefined, "metricAgg": "max", @@ -4061,6 +4178,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Merge Rate", "mbField": undefined, "metricAgg": "max", @@ -4109,6 +4227,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Search Latency", "mbField": undefined, "metricAgg": "sum", @@ -4132,6 +4251,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Total Refresh Time", "mbField": undefined, "metricAgg": "max", @@ -4155,6 +4275,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Primary Shards", "mbField": undefined, "metricAgg": "max", @@ -4179,6 +4300,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Total Shards", "mbField": undefined, "metricAgg": "max", @@ -4203,6 +4325,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Total Shards", "mbField": undefined, "metricAgg": "max", @@ -4227,6 +4350,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Search", "mbField": undefined, "metricAgg": "max", @@ -4251,6 +4375,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Search Total", "mbField": undefined, "metricAgg": "max", @@ -4275,6 +4400,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Primaries", "mbField": undefined, "metricAgg": "max", @@ -4299,6 +4425,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Total", "mbField": undefined, "metricAgg": "max", @@ -4323,6 +4450,7 @@ Object { "fieldSource": undefined, "format": "0,0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Merges (Primaries)", "mbField": undefined, "metricAgg": "max", @@ -4347,6 +4475,7 @@ Object { "fieldSource": undefined, "format": "0,0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Merges", "mbField": undefined, "metricAgg": "max", @@ -4371,6 +4500,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Primaries", "mbField": undefined, "metricAgg": "max", @@ -4395,6 +4525,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Total", "mbField": undefined, "metricAgg": "max", @@ -4419,6 +4550,7 @@ Object { "fieldSource": undefined, "format": "0,0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Store (Primaries)", "mbField": undefined, "metricAgg": "max", @@ -4443,6 +4575,7 @@ Object { "fieldSource": undefined, "format": "0,0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Store", "mbField": undefined, "metricAgg": "max", @@ -4467,6 +4600,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Index Throttling Time", "mbField": undefined, "metricAgg": "max", @@ -4490,6 +4624,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Indexing (Primaries)", "mbField": undefined, "metricAgg": "max", @@ -4514,6 +4649,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Indexing", "mbField": undefined, "metricAgg": "max", @@ -4538,6 +4674,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "HTTP Connections", "mbField": undefined, "metricAgg": "max", @@ -4560,6 +4697,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Average", "mbField": undefined, "metricAgg": "max", @@ -4571,6 +4709,75 @@ Object { "usageField": undefined, "uuidField": "kibana_stats.kibana.uuid", }, + "kibana_cluster_action_overdue_count": KibanaClusterActionMetric { + "aggs": undefined, + "app": "kibana", + "calculation": undefined, + "dateHistogramSubAggs": undefined, + "derivative": false, + "description": "Number of overdue actions across the entire cluster.", + "docType": undefined, + "field": "kibana.cluster_actions.overdue.count", + "fieldSource": undefined, + "format": "0.[00]", + "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": true, + "label": "Action Overdue Count", + "mbField": undefined, + "metricAgg": "max", + "periodsField": undefined, + "quotaField": undefined, + "timestampField": "timestamp", + "units": "", + "usageField": undefined, + "uuidField": "cluster_uuid", + }, + "kibana_cluster_action_overdue_p50": KibanaClusterActionMetric { + "aggs": undefined, + "app": "kibana", + "calculation": undefined, + "dateHistogramSubAggs": undefined, + "derivative": false, + "description": "Average delay of all overdue actions across the entire cluster.", + "docType": undefined, + "field": "kibana.cluster_actions.overdue.delay.p50", + "fieldSource": undefined, + "format": "0.[00]", + "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": true, + "label": "Average Action Overdue Delay", + "mbField": undefined, + "metricAgg": "max", + "periodsField": undefined, + "quotaField": undefined, + "timestampField": "timestamp", + "units": "ms", + "usageField": undefined, + "uuidField": "cluster_uuid", + }, + "kibana_cluster_action_overdue_p99": KibanaClusterActionMetric { + "aggs": undefined, + "app": "kibana", + "calculation": undefined, + "dateHistogramSubAggs": undefined, + "derivative": false, + "description": "Worst delay of all overdue actions across the entire cluster.", + "docType": undefined, + "field": "kibana.cluster_actions.overdue.delay.p99", + "fieldSource": undefined, + "format": "0.[00]", + "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": true, + "label": "Worst Action Overdue Delay", + "mbField": undefined, + "metricAgg": "max", + "periodsField": undefined, + "quotaField": undefined, + "timestampField": "timestamp", + "units": "ms", + "usageField": undefined, + "uuidField": "cluster_uuid", + }, "kibana_cluster_average_response_times": KibanaEventsRateClusterMetric { "aggs": Object { "event_rate": Object { @@ -4610,6 +4817,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Average", "mbField": undefined, "metricAgg": "max", @@ -4660,6 +4868,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Max", "mbField": undefined, "metricAgg": "max", @@ -4710,6 +4919,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Client Requests", "mbField": undefined, "metricAgg": "max", @@ -4720,6 +4930,167 @@ Object { "usageField": undefined, "uuidField": "cluster_uuid", }, + "kibana_cluster_rule_overdue_count": KibanaClusterRuleMetric { + "aggs": undefined, + "app": "kibana", + "calculation": undefined, + "dateHistogramSubAggs": undefined, + "derivative": false, + "description": "Number of overdue rules across the entire cluster.", + "docType": undefined, + "field": "kibana.cluster_rules.overdue.count", + "fieldSource": undefined, + "format": "0.[00]", + "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": true, + "label": "Rule Overdue Count", + "mbField": undefined, + "metricAgg": "max", + "periodsField": undefined, + "quotaField": undefined, + "timestampField": "timestamp", + "units": "", + "usageField": undefined, + "uuidField": "cluster_uuid", + }, + "kibana_cluster_rule_overdue_p50": KibanaClusterRuleMetric { + "aggs": undefined, + "app": "kibana", + "calculation": undefined, + "dateHistogramSubAggs": undefined, + "derivative": false, + "description": "Average delay of all overdue rules across the entire cluster.", + "docType": undefined, + "field": "kibana.cluster_rules.overdue.delay.p50", + "fieldSource": undefined, + "format": "0.[00]", + "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": true, + "label": "Average Rule Overdue Delay", + "mbField": undefined, + "metricAgg": "max", + "periodsField": undefined, + "quotaField": undefined, + "timestampField": "timestamp", + "units": "ms", + "usageField": undefined, + "uuidField": "cluster_uuid", + }, + "kibana_cluster_rule_overdue_p99": KibanaClusterRuleMetric { + "aggs": undefined, + "app": "kibana", + "calculation": undefined, + "dateHistogramSubAggs": undefined, + "derivative": false, + "description": "Worst delay of all overdue rules across the entire cluster.", + "docType": undefined, + "field": "kibana.cluster_rules.overdue.delay.p99", + "fieldSource": undefined, + "format": "0.[00]", + "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": true, + "label": "Worst Rule Overdue Delay", + "mbField": undefined, + "metricAgg": "max", + "periodsField": undefined, + "quotaField": undefined, + "timestampField": "timestamp", + "units": "ms", + "usageField": undefined, + "uuidField": "cluster_uuid", + }, + "kibana_instance_action_executions": KibanaInstanceActionMetric { + "aggs": undefined, + "app": "kibana", + "calculation": undefined, + "dateHistogramSubAggs": undefined, + "derivative": true, + "description": "Rate of action executions for the Kibana instance.", + "docType": undefined, + "field": "kibana.node_actions.executions", + "fieldSource": undefined, + "format": "0.[00]", + "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": true, + "label": "Action Executions Rate", + "mbField": undefined, + "metricAgg": "max", + "periodsField": undefined, + "quotaField": undefined, + "timestampField": "timestamp", + "units": "", + "usageField": undefined, + "uuidField": "kibana_stats.kibana.uuid", + }, + "kibana_instance_action_failures": KibanaInstanceActionMetric { + "aggs": undefined, + "app": "kibana", + "calculation": undefined, + "dateHistogramSubAggs": undefined, + "derivative": true, + "description": "Rate of action failures for the Kibana instance.", + "docType": undefined, + "field": "kibana.node_actions.failures", + "fieldSource": undefined, + "format": "0.[00]", + "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": true, + "label": "Action Failures Rate", + "mbField": undefined, + "metricAgg": "max", + "periodsField": undefined, + "quotaField": undefined, + "timestampField": "timestamp", + "units": "", + "usageField": undefined, + "uuidField": "kibana_stats.kibana.uuid", + }, + "kibana_instance_rule_executions": KibanaInstanceRuleMetric { + "aggs": undefined, + "app": "kibana", + "calculation": undefined, + "dateHistogramSubAggs": undefined, + "derivative": true, + "description": "Rate of rule executions for the Kibana instance.", + "docType": undefined, + "field": "kibana.node_rules.executions", + "fieldSource": undefined, + "format": "0.[00]", + "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": true, + "label": "Rule Executions Rate", + "mbField": undefined, + "metricAgg": "max", + "periodsField": undefined, + "quotaField": undefined, + "timestampField": "timestamp", + "units": "", + "usageField": undefined, + "uuidField": "kibana_stats.kibana.uuid", + }, + "kibana_instance_rule_failures": KibanaInstanceRuleMetric { + "aggs": undefined, + "app": "kibana", + "calculation": undefined, + "dateHistogramSubAggs": undefined, + "derivative": true, + "description": "Rate of rule failures for the Kibana instance.", + "docType": undefined, + "field": "kibana.node_rules.failures", + "fieldSource": undefined, + "format": "0.[00]", + "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": true, + "label": "Rule Failures Rate", + "mbField": undefined, + "metricAgg": "max", + "periodsField": undefined, + "quotaField": undefined, + "timestampField": "timestamp", + "units": "", + "usageField": undefined, + "uuidField": "kibana_stats.kibana.uuid", + }, "kibana_max_response_times": KibanaMetric { "aggs": undefined, "app": "kibana", @@ -4732,6 +5103,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Max", "mbField": undefined, "metricAgg": "max", @@ -4755,6 +5127,7 @@ Object { "fieldSource": undefined, "format": "0,0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Heap Size Limit", "mbField": undefined, "metricAgg": "max", @@ -4778,6 +5151,7 @@ Object { "fieldSource": undefined, "format": "0,0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Memory Size", "mbField": undefined, "metricAgg": "max", @@ -4801,6 +5175,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "15m", "mbField": undefined, "metricAgg": "max", @@ -4824,6 +5199,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "1m", "mbField": undefined, "metricAgg": "max", @@ -4847,6 +5223,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "5m", "mbField": undefined, "metricAgg": "max", @@ -4870,6 +5247,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Event Loop Delay", "mbField": undefined, "metricAgg": "max", @@ -4892,6 +5270,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Client Disconnects", "mbField": undefined, "metricAgg": "max", @@ -4914,6 +5293,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Client Requests", "mbField": undefined, "metricAgg": "max", @@ -4963,6 +5343,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Events Received Rate", "mbField": undefined, "metricAgg": "max", @@ -5030,6 +5411,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Event Latency", "mbField": undefined, "metricAgg": "max", @@ -5079,6 +5461,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Events Emitted Rate", "mbField": undefined, "metricAgg": "max", @@ -5101,6 +5484,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": [Function], + "isNotSupportedInInternalCollection": undefined, "label": "Pipeline Node Count", "mbField": undefined, "metricAgg": undefined, @@ -5123,6 +5507,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": [Function], + "isNotSupportedInInternalCollection": undefined, "label": "Pipeline Throughput", "mbField": "logstash.node.stats.pipelines.events.out", "metricAgg": undefined, @@ -5145,6 +5530,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Events Received Rate", "mbField": undefined, "metricAgg": "max", @@ -5192,6 +5578,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Event Latency", "mbField": undefined, "metricAgg": "sum", @@ -5214,6 +5601,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Events Emitted Rate", "mbField": undefined, "metricAgg": "max", @@ -5236,6 +5624,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Cgroup Elapsed Periods", "mbField": undefined, "metricAgg": "max", @@ -5289,6 +5678,7 @@ Object { "fieldSource": "logstash_stats.os.cgroup", "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Cgroup CPU Utilization", "mbField": undefined, "metricAgg": "max", @@ -5342,6 +5732,7 @@ Object { "fieldSource": "logstash_stats.os.cgroup", "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "CPU Utilization", "mbField": undefined, "metricAgg": "max", @@ -5364,6 +5755,7 @@ Object { "fieldSource": undefined, "format": "0,0.[0]a", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Cgroup Throttling", "mbField": undefined, "metricAgg": "max", @@ -5387,6 +5779,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Cgroup Throttled Count", "mbField": undefined, "metricAgg": "max", @@ -5410,6 +5803,7 @@ Object { "fieldSource": undefined, "format": "0,0.[0]a", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Cgroup Usage", "mbField": undefined, "metricAgg": "max", @@ -5433,6 +5827,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "CPU Utilization", "mbField": undefined, "metricAgg": "max", @@ -5455,6 +5850,7 @@ Object { "fieldSource": undefined, "format": "0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Max Heap", "mbField": undefined, "metricAgg": "max", @@ -5478,6 +5874,7 @@ Object { "fieldSource": undefined, "format": "0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Used Heap", "mbField": undefined, "metricAgg": "max", @@ -5501,6 +5898,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": [Function], + "isNotSupportedInInternalCollection": undefined, "label": "Pipeline Node Count", "mbField": undefined, "metricAgg": undefined, @@ -5523,6 +5921,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": [Function], + "isNotSupportedInInternalCollection": undefined, "label": "Pipeline Throughput", "mbField": "logstash.node.stats.pipelines.events.out", "metricAgg": undefined, @@ -5545,6 +5944,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "15m", "mbField": undefined, "metricAgg": "max", @@ -5568,6 +5968,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "1m", "mbField": undefined, "metricAgg": "max", @@ -5591,6 +5992,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "5m", "mbField": undefined, "metricAgg": "max", @@ -5640,6 +6042,7 @@ Object { "fieldSource": undefined, "format": "0,0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Max Queue Size", "mbField": undefined, "metricAgg": undefined, @@ -5688,6 +6091,7 @@ Object { "fieldSource": undefined, "format": "0,0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Queue Size", "mbField": undefined, "metricAgg": undefined, @@ -5711,6 +6115,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Events Queued", "mbField": undefined, "metricAgg": "max", @@ -5734,6 +6139,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Cgroup Elapsed Periods", "mbField": undefined, "metricAgg": "max", @@ -5788,6 +6194,7 @@ Object { "fieldSource": "node_stats.os.cgroup", "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Cgroup CPU Utilization", "mbField": undefined, "metricAgg": "max", @@ -5842,6 +6249,7 @@ Object { "fieldSource": "node_stats.os.cgroup", "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "CPU Utilization", "mbField": undefined, "metricAgg": "max", @@ -5865,6 +6273,7 @@ Object { "fieldSource": undefined, "format": "0,0.[0]a", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Cgroup Throttling", "mbField": undefined, "metricAgg": "max", @@ -5889,6 +6298,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Cgroup Throttled Count", "mbField": undefined, "metricAgg": "max", @@ -5913,6 +6323,7 @@ Object { "fieldSource": undefined, "format": "0,0.[0]a", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Cgroup Usage", "mbField": undefined, "metricAgg": "max", @@ -5937,6 +6348,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "CPU Utilization", "mbField": undefined, "metricAgg": "max", @@ -5960,6 +6372,7 @@ Object { "fieldSource": undefined, "format": "0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Disk Free Space", "mbField": undefined, "metricAgg": "max", @@ -6008,6 +6421,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Indexing", "mbField": undefined, "metricAgg": "sum", @@ -6032,6 +6446,7 @@ Object { "fieldSource": undefined, "format": "0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Fielddata", "mbField": undefined, "metricAgg": "max", @@ -6056,6 +6471,7 @@ Object { "fieldSource": undefined, "format": "0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Fixed Bitsets", "mbField": undefined, "metricAgg": "max", @@ -6080,6 +6496,7 @@ Object { "fieldSource": undefined, "format": "0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Query Cache", "mbField": undefined, "metricAgg": "max", @@ -6104,6 +6521,7 @@ Object { "fieldSource": undefined, "format": "0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Request Cache", "mbField": undefined, "metricAgg": "max", @@ -6128,6 +6546,7 @@ Object { "fieldSource": undefined, "format": "0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Version Map", "mbField": undefined, "metricAgg": "max", @@ -6152,6 +6571,7 @@ Object { "fieldSource": undefined, "format": "0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Index Writer", "mbField": undefined, "metricAgg": "max", @@ -6176,6 +6596,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "GET Queue", "mbField": undefined, "metricAgg": "max", @@ -6201,6 +6622,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "GET Rejections", "mbField": undefined, "metricAgg": "max", @@ -6226,6 +6648,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Search Queue", "mbField": undefined, "metricAgg": "max", @@ -6251,6 +6674,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Search Rejections", "mbField": undefined, "metricAgg": "max", @@ -6292,6 +6716,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Write Queue", "mbField": undefined, "metricAgg": "max", @@ -6353,6 +6778,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Write Rejections", "mbField": undefined, "metricAgg": "max", @@ -6377,6 +6803,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Index Time", "mbField": undefined, "metricAgg": "max", @@ -6401,6 +6828,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Indexing Total", "mbField": undefined, "metricAgg": "max", @@ -6425,6 +6853,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Old", "mbField": undefined, "metricAgg": "max", @@ -6449,6 +6878,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Old", "mbField": undefined, "metricAgg": "max", @@ -6473,6 +6903,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Young", "mbField": undefined, "metricAgg": "max", @@ -6497,6 +6928,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Young", "mbField": undefined, "metricAgg": "max", @@ -6521,6 +6953,7 @@ Object { "fieldSource": undefined, "format": "0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Max Heap", "mbField": undefined, "metricAgg": "max", @@ -6545,6 +6978,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Used Heap", "mbField": undefined, "metricAgg": "max", @@ -6569,6 +7003,7 @@ Object { "fieldSource": undefined, "format": "0.0 b", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Used Heap", "mbField": undefined, "metricAgg": "max", @@ -6593,6 +7028,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "1m", "mbField": undefined, "metricAgg": "max", @@ -6642,6 +7078,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Search", "mbField": undefined, "metricAgg": "sum", @@ -6666,6 +7103,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Search Total", "mbField": undefined, "metricAgg": "max", @@ -6690,6 +7128,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Segment Count", "mbField": undefined, "metricAgg": "max", @@ -6713,6 +7152,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Bulk", "mbField": undefined, "metricAgg": "max", @@ -6737,6 +7177,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Generic", "mbField": undefined, "metricAgg": "max", @@ -6761,6 +7202,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Get", "mbField": undefined, "metricAgg": "max", @@ -6785,6 +7227,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Index", "mbField": undefined, "metricAgg": "max", @@ -6809,6 +7252,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Management", "mbField": undefined, "metricAgg": "max", @@ -6833,6 +7277,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Search", "mbField": undefined, "metricAgg": "max", @@ -6857,6 +7302,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Watcher", "mbField": undefined, "metricAgg": "max", @@ -6881,6 +7327,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Bulk", "mbField": undefined, "metricAgg": "max", @@ -6905,6 +7352,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Generic", "mbField": undefined, "metricAgg": "max", @@ -6929,6 +7377,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Get", "mbField": undefined, "metricAgg": "max", @@ -6953,6 +7402,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Index", "mbField": undefined, "metricAgg": "max", @@ -6977,6 +7427,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Management", "mbField": undefined, "metricAgg": "max", @@ -7001,6 +7452,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Search", "mbField": undefined, "metricAgg": "max", @@ -7025,6 +7477,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Watcher", "mbField": undefined, "metricAgg": "max", @@ -7049,6 +7502,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Index Throttling Time", "mbField": undefined, "metricAgg": "max", @@ -7074,6 +7528,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Total I/O", "mbField": undefined, "metricAgg": "max", @@ -7098,6 +7553,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Total Read I/O", "mbField": undefined, "metricAgg": "max", @@ -7122,6 +7578,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Total Write I/O", "mbField": undefined, "metricAgg": "max", @@ -7146,6 +7603,7 @@ Object { "fieldSource": undefined, "format": "0,0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Total Shards", "mbField": undefined, "metricAgg": "max", @@ -7170,6 +7628,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Org Sources", "mbField": undefined, "metricAgg": "avg", @@ -7193,6 +7652,7 @@ Object { "fieldSource": undefined, "format": "0.[00]", "getDateHistogramSubAggs": undefined, + "isNotSupportedInInternalCollection": undefined, "label": "Private Sources", "mbField": undefined, "metricAgg": "avg", diff --git a/x-pack/plugins/monitoring/server/lib/metrics/classes/metric.ts b/x-pack/plugins/monitoring/server/lib/metrics/classes/metric.ts index 8c2c54cd2f4ed..e9c04d7e0aebf 100644 --- a/x-pack/plugins/monitoring/server/lib/metrics/classes/metric.ts +++ b/x-pack/plugins/monitoring/server/lib/metrics/classes/metric.ts @@ -23,6 +23,7 @@ interface OptionalMetricOptions { metricAgg?: string; mbField?: string; type?: string; + isNotSupportedInInternalCollection?: boolean; } interface DefaultMetricOptions { @@ -56,6 +57,7 @@ export class Metric { public usageField?: string; public periodsField?: string; public quotaField?: string; + public isNotSupportedInInternalCollection?: boolean; constructor(opts: MetricOptions) { const props: Required = { diff --git a/x-pack/plugins/monitoring/server/lib/metrics/index.ts b/x-pack/plugins/monitoring/server/lib/metrics/index.ts index 12c19cdd4b870..6092d78b2659e 100644 --- a/x-pack/plugins/monitoring/server/lib/metrics/index.ts +++ b/x-pack/plugins/monitoring/server/lib/metrics/index.ts @@ -6,7 +6,12 @@ */ export { ElasticsearchMetric } from './elasticsearch/classes'; -export { KibanaClusterMetric, KibanaMetric } from './kibana/classes'; +export { + KibanaClusterMetric, + KibanaMetric, + KibanaClusterRuleMetric, + KibanaClusterActionMetric, +} from './kibana/classes'; export type { ApmMetricFields } from './apm/classes'; export { ApmMetric, ApmClusterMetric } from './apm/classes'; export { LogstashClusterMetric, LogstashMetric } from './logstash/classes'; diff --git a/x-pack/plugins/monitoring/server/lib/metrics/kibana/classes.ts b/x-pack/plugins/monitoring/server/lib/metrics/kibana/classes.ts index 84c2d60b43fe2..4a171ce6ce40f 100644 --- a/x-pack/plugins/monitoring/server/lib/metrics/kibana/classes.ts +++ b/x-pack/plugins/monitoring/server/lib/metrics/kibana/classes.ts @@ -12,7 +12,14 @@ import { NORMALIZED_DERIVATIVE_UNIT } from '../../../../common/constants'; type KibanaClusterMetricOptions = Pick< MetricOptions, - 'field' | 'label' | 'description' | 'format' | 'units' | 'metricAgg' + | 'field' + | 'label' + | 'description' + | 'format' + | 'units' + | 'metricAgg' + | 'derivative' + | 'isNotSupportedInInternalCollection' > & Partial>; @@ -33,11 +40,73 @@ export class KibanaClusterMetric extends ClusterMetric { } } -type KibanaEventsRateClusterMetricOptions = Pick< - MetricOptions, - 'field' | 'label' | 'description' | 'format' | 'units' -> & - Partial>; +export class KibanaClusterRuleMetric extends ClusterMetric { + constructor(opts: KibanaClusterMetricOptions) { + super({ + ...opts, + app: 'kibana', + ...KibanaClusterRuleMetric.getMetricFields(), + }); + } + + static getMetricFields() { + return { + uuidField: 'cluster_uuid', + timestampField: 'timestamp', // This will alias to @timestamp + }; + } +} + +export class KibanaInstanceRuleMetric extends Metric { + constructor(opts: KibanaClusterMetricOptions) { + super({ + ...opts, + app: 'kibana', + ...KibanaInstanceRuleMetric.getMetricFields(), + }); + } + + static getMetricFields() { + return { + uuidField: 'kibana_stats.kibana.uuid', // This field does not exist in the MB document but the alias exists + timestampField: 'timestamp', // This will alias to @timestamp + }; + } +} + +export class KibanaClusterActionMetric extends ClusterMetric { + constructor(opts: KibanaClusterMetricOptions) { + super({ + ...opts, + app: 'kibana', + ...KibanaClusterActionMetric.getMetricFields(), + }); + } + + static getMetricFields() { + return { + uuidField: 'cluster_uuid', + timestampField: 'timestamp', // This will alias to @timestamp + }; + } +} + +export class KibanaInstanceActionMetric extends Metric { + constructor(opts: KibanaClusterMetricOptions) { + super({ + ...opts, + app: 'kibana', + ...KibanaInstanceActionMetric.getMetricFields(), + }); + } + + static getMetricFields() { + return { + uuidField: 'kibana_stats.kibana.uuid', // This field does not exist in the MB document but the alias exists + timestampField: 'timestamp', // This will alias to @timestamp + }; + } +} export class KibanaEventsRateClusterMetric extends KibanaClusterMetric { constructor(opts: KibanaEventsRateClusterMetricOptions) { @@ -77,6 +146,12 @@ export class KibanaEventsRateClusterMetric extends KibanaClusterMetric { } } +type KibanaEventsRateClusterMetricOptions = Pick< + MetricOptions, + 'field' | 'label' | 'description' | 'format' | 'units' +> & + Partial>; + type KibanaMetricOptions = Pick< MetricOptions, 'field' | 'label' | 'description' | 'format' | 'metricAgg' | 'units' diff --git a/x-pack/plugins/monitoring/server/lib/metrics/kibana/metrics.ts b/x-pack/plugins/monitoring/server/lib/metrics/kibana/metrics.ts index e6db13e222251..022f81f5854ac 100644 --- a/x-pack/plugins/monitoring/server/lib/metrics/kibana/metrics.ts +++ b/x-pack/plugins/monitoring/server/lib/metrics/kibana/metrics.ts @@ -6,7 +6,14 @@ */ import { i18n } from '@kbn/i18n'; -import { KibanaEventsRateClusterMetric, KibanaMetric } from './classes'; +import { + KibanaEventsRateClusterMetric, + KibanaMetric, + KibanaClusterRuleMetric, + KibanaInstanceRuleMetric, + KibanaInstanceActionMetric, + KibanaClusterActionMetric, +} from './classes'; import { LARGE_FLOAT, SMALL_FLOAT, LARGE_BYTES } from '../../../../common/formatting'; const clientResponseTimeTitle = i18n.translate( @@ -253,4 +260,173 @@ export const metrics = { metricAgg: 'max', units: '', }), + + kibana_instance_rule_failures: new KibanaInstanceRuleMetric({ + derivative: true, + field: 'kibana.node_rules.failures', + label: i18n.translate('xpack.monitoring.metrics.kibanaInstance.ruleInstanceFailuresLabel', { + defaultMessage: 'Rule Failures Rate', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.kibanaInstance.ruleInstanceFailuresDescription', + { + defaultMessage: 'Rate of rule failures for the Kibana instance.', + } + ), + format: SMALL_FLOAT, + metricAgg: 'max', + units: '', + isNotSupportedInInternalCollection: true, + }), + kibana_instance_rule_executions: new KibanaInstanceRuleMetric({ + derivative: true, + field: 'kibana.node_rules.executions', + label: i18n.translate('xpack.monitoring.metrics.kibanaInstance.ruleInstanceExecutionsLabel', { + defaultMessage: 'Rule Executions Rate', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.kibanaInstance.ruleInstanceExecutionsDescription', + { + defaultMessage: 'Rate of rule executions for the Kibana instance.', + } + ), + format: SMALL_FLOAT, + metricAgg: 'max', + units: '', + isNotSupportedInInternalCollection: true, + }), + kibana_instance_action_failures: new KibanaInstanceActionMetric({ + derivative: true, + field: 'kibana.node_actions.failures', + label: i18n.translate('xpack.monitoring.metrics.kibanaInstance.actionInstanceFailuresLabel', { + defaultMessage: 'Action Failures Rate', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.kibanaInstance.actionInstanceFailuresDescription', + { + defaultMessage: 'Rate of action failures for the Kibana instance.', + } + ), + format: SMALL_FLOAT, + metricAgg: 'max', + units: '', + isNotSupportedInInternalCollection: true, + }), + kibana_instance_action_executions: new KibanaInstanceActionMetric({ + derivative: true, + field: 'kibana.node_actions.executions', + label: i18n.translate('xpack.monitoring.metrics.kibanaInstance.actionInstanceExecutionsLabel', { + defaultMessage: 'Action Executions Rate', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.kibanaInstance.actionInstanceExecutionsDescription', + { + defaultMessage: 'Rate of action executions for the Kibana instance.', + } + ), + format: SMALL_FLOAT, + metricAgg: 'max', + units: '', + isNotSupportedInInternalCollection: true, + }), + + kibana_cluster_rule_overdue_count: new KibanaClusterRuleMetric({ + field: 'kibana.cluster_rules.overdue.count', + label: i18n.translate('xpack.monitoring.metrics.kibanaInstance.clusterRuleOverdueCountLabel', { + defaultMessage: 'Rule Overdue Count', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.kibanaInstance.clusterRuleOverdueCountDescription', + { + defaultMessage: 'Number of overdue rules across the entire cluster.', + } + ), + format: SMALL_FLOAT, + metricAgg: 'max', + units: '', + isNotSupportedInInternalCollection: true, + }), + kibana_cluster_rule_overdue_p50: new KibanaClusterRuleMetric({ + field: 'kibana.cluster_rules.overdue.delay.p50', + label: i18n.translate('xpack.monitoring.metrics.kibanaInstance.clusterRuleOverdueP50Label', { + defaultMessage: 'Average Rule Overdue Delay', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.kibanaInstance.clusterRuleOverdueP50Description', + { + defaultMessage: 'Average delay of all overdue rules across the entire cluster.', + } + ), + format: SMALL_FLOAT, + metricAgg: 'max', + units: msTimeUnitLabel, + isNotSupportedInInternalCollection: true, + }), + kibana_cluster_rule_overdue_p99: new KibanaClusterRuleMetric({ + field: 'kibana.cluster_rules.overdue.delay.p99', + label: i18n.translate('xpack.monitoring.metrics.kibanaInstance.clusterRuleOverdueP99Label', { + defaultMessage: 'Worst Rule Overdue Delay', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.kibanaInstance.clusterRuleOverdueP99Description', + { + defaultMessage: 'Worst delay of all overdue rules across the entire cluster.', + } + ), + format: SMALL_FLOAT, + metricAgg: 'max', + units: msTimeUnitLabel, + isNotSupportedInInternalCollection: true, + }), + kibana_cluster_action_overdue_count: new KibanaClusterActionMetric({ + field: 'kibana.cluster_actions.overdue.count', + label: i18n.translate( + 'xpack.monitoring.metrics.kibanaInstance.clusterActionOverdueCountLabel', + { + defaultMessage: 'Action Overdue Count', + } + ), + description: i18n.translate( + 'xpack.monitoring.metrics.kibanaInstance.clusterActionOverdueCountDescription', + { + defaultMessage: 'Number of overdue actions across the entire cluster.', + } + ), + format: SMALL_FLOAT, + metricAgg: 'max', + units: '', + isNotSupportedInInternalCollection: true, + }), + kibana_cluster_action_overdue_p50: new KibanaClusterActionMetric({ + field: 'kibana.cluster_actions.overdue.delay.p50', + label: i18n.translate('xpack.monitoring.metrics.kibanaInstance.clusterActionOverdueP50Label', { + defaultMessage: 'Average Action Overdue Delay', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.kibanaInstance.clusterActionOverdueP50Description', + { + defaultMessage: 'Average delay of all overdue actions across the entire cluster.', + } + ), + format: SMALL_FLOAT, + metricAgg: 'max', + units: msTimeUnitLabel, + isNotSupportedInInternalCollection: true, + }), + kibana_cluster_action_overdue_p99: new KibanaClusterActionMetric({ + field: 'kibana.cluster_actions.overdue.delay.p99', + label: i18n.translate('xpack.monitoring.metrics.kibanaInstance.clusterActionOverdueP99Label', { + defaultMessage: 'Worst Action Overdue Delay', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.kibanaInstance.clusterActionOverdueP99Description', + { + defaultMessage: 'Worst delay of all overdue actions across the entire cluster.', + } + ), + format: SMALL_FLOAT, + metricAgg: 'max', + units: msTimeUnitLabel, + isNotSupportedInInternalCollection: true, + }), }; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/metric_set_instance.ts b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/metric_set_instance.ts index 0817c35bb2d36..ca258d1ab97b4 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/metric_set_instance.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/metric_set_instance.ts @@ -26,4 +26,8 @@ export const metricSet: MetricDescriptor[] = [ keys: ['kibana_requests_total', 'kibana_requests_disconnects'], name: 'kibana_requests', }, + 'kibana_instance_rule_failures', + 'kibana_instance_rule_executions', + 'kibana_instance_action_failures', + 'kibana_instance_action_executions', ]; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/metric_set_overview.ts b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/metric_set_overview.ts index 126b220801e2e..2369d0fab2ba2 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/metric_set_overview.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/metric_set_overview.ts @@ -13,4 +13,14 @@ export const metricSet: MetricDescriptor[] = [ keys: ['kibana_cluster_max_response_times', 'kibana_cluster_average_response_times'], name: 'kibana_cluster_response_times', }, + 'kibana_cluster_rule_overdue_count', + { + keys: ['kibana_cluster_rule_overdue_p50', 'kibana_cluster_rule_overdue_p99'], + name: 'kibana_cluster_rule_overdue_duration', + }, + 'kibana_cluster_action_overdue_count', + { + keys: ['kibana_cluster_action_overdue_p50', 'kibana_cluster_action_overdue_p99'], + name: 'kibana_cluster_action_overdue_duration', + }, ]; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/overview.ts b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/overview.ts index 508237ea50bc2..7330ccd27d51a 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/overview.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/overview.ts @@ -33,18 +33,25 @@ export function kibanaOverviewRoute(server: MonitoringCore) { try { const moduleType = 'kibana'; - const dsDataset = 'stats'; + const dsDatasets = ['stats', 'cluster_rules', 'cluster_actions']; + const bools = dsDatasets.reduce( + (accum: Array<{ term: { [key: string]: string } }>, dsDataset) => { + accum.push( + ...[ + { term: { 'data_stream.dataset': `${moduleType}.${dsDataset}` } }, + { term: { 'metricset.name': dsDataset } }, + { term: { type: `kibana_${dsDataset}` } }, + ] + ); + return accum; + }, + [] + ); const [clusterStatus, metrics] = await Promise.all([ getKibanaClusterStatus(req, { clusterUuid }), getMetrics(req, moduleType, metricSet, [ { - bool: { - should: [ - { term: { 'data_stream.dataset': `${moduleType}.${dsDataset}` } }, - { term: { 'metricset.name': dsDataset } }, - { term: { type: 'kibana_stats' } }, - ], - }, + bool: { should: bools }, }, ]), ]); diff --git a/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/multicluster.json b/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/multicluster.json index 5d28989f9010f..453ad92e1b556 100644 --- a/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/multicluster.json +++ b/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/multicluster.json @@ -3,41 +3,21 @@ "cluster_uuid": "6d-9tDFTRe-qT5GoBytdlQ", "cluster_name": "clustertwo", "version": "7.0.0-alpha1", - "license": { - "status": "active", - "type": "basic", - "expiry_date_in_millis": 1914278399999 - }, + "license": { "status": "active", "type": "basic", "expiry_date_in_millis": 1914278399999 }, "elasticsearch": { "cluster_stats": { "indices": { "count": 1, - "docs": { - "deleted": 0, - "count": 8 - }, - "shards": { - "total": 1, - "primaries": 1 - }, - "store": { - "size_in_bytes": 34095 - } + "docs": { "deleted": 0, "count": 8 }, + "shards": { "total": 1, "primaries": 1 }, + "store": { "size_in_bytes": 34095 } }, "nodes": { - "fs": { - "total_in_bytes": 499065712640, - "available_in_bytes": 200403197952 - }, - "count": { - "total": 1 - }, + "fs": { "total_in_bytes": 499065712640, "available_in_bytes": 200403197952 }, + "count": { "total": 1 }, "jvm": { "max_uptime_in_millis": 701043, - "mem": { - "heap_max_in_bytes": 628555776, - "heap_used_in_bytes": 204299464 - } + "mem": { "heap_max_in_bytes": 628555776, "heap_used_in_bytes": 204299464 } } }, "status": "green" @@ -51,10 +31,7 @@ "avg_memory_used": 0, "max_uptime": 0, "pipeline_count": 0, - "queue_types": { - "memory": 0, - "persisted": 0 - }, + "queue_types": { "memory": 0, "persisted": 0 }, "versions": [] }, "kibana": { @@ -65,26 +42,16 @@ "response_time_max": 0, "memory_size": 0, "memory_limit": 0, - "count": 0 - }, - "beats": { - "totalEvents": 0, - "bytesSent": 0, - "beats": { - "total": 0, - "types": [] - } + "count": 0, + "rules": { "cluster": null, "instance": null } }, + "beats": { "totalEvents": 0, "bytesSent": 0, "beats": { "total": 0, "types": [] } }, "apm": { "totalEvents": 0, "memRss": 0, - "apms": { - "total": 0 - }, + "apms": { "total": 0 }, "versions": [], - "config": { - "container": false - } + "config": { "container": false } }, "enterpriseSearch": { "clusterUuid": "6d-9tDFTRe-qT5GoBytdlQ", @@ -100,12 +67,7 @@ "versions": [] } }, - "alerts": { - "list": {}, - "alertsMeta": { - "enabled": true - } - }, + "alerts": { "list": {}, "alertsMeta": { "enabled": true } }, "isPrimary": false, "status": "green", "isCcrEnabled": true @@ -115,41 +77,21 @@ "cluster_uuid": "lOF8kofiS_2DX58o9mXJ1Q", "cluster_name": "monitoring-one", "version": "7.0.0-alpha1", - "license": { - "status": "active", - "type": "trial", - "expiry_date_in_millis": 1505426308997 - }, + "license": { "status": "active", "type": "trial", "expiry_date_in_millis": 1505426308997 }, "elasticsearch": { "cluster_stats": { "indices": { "count": 8, - "docs": { - "deleted": 69, - "count": 3997 - }, - "shards": { - "total": 8, - "primaries": 8 - }, - "store": { - "size_in_bytes": 2647163 - } + "docs": { "deleted": 69, "count": 3997 }, + "shards": { "total": 8, "primaries": 8 }, + "store": { "size_in_bytes": 2647163 } }, "nodes": { - "fs": { - "total_in_bytes": 499065712640, - "available_in_bytes": 200403648512 - }, - "count": { - "total": 1 - }, + "fs": { "total_in_bytes": 499065712640, "available_in_bytes": 200403648512 }, + "count": { "total": 1 }, "jvm": { "max_uptime_in_millis": 761002, - "mem": { - "heap_max_in_bytes": 628555776, - "heap_used_in_bytes": 133041176 - } + "mem": { "heap_max_in_bytes": 628555776, "heap_used_in_bytes": 133041176 } } }, "status": "yellow" @@ -163,10 +105,7 @@ "avg_memory_used": 0, "max_uptime": 0, "pipeline_count": 0, - "queue_types": { - "memory": 0, - "persisted": 0 - }, + "queue_types": { "memory": 0, "persisted": 0 }, "versions": [] }, "kibana": { @@ -177,26 +116,16 @@ "response_time_max": 0, "memory_size": 0, "memory_limit": 0, - "count": 0 - }, - "beats": { - "totalEvents": 0, - "bytesSent": 0, - "beats": { - "total": 0, - "types": [] - } + "count": 0, + "rules": { "cluster": null, "instance": null } }, + "beats": { "totalEvents": 0, "bytesSent": 0, "beats": { "total": 0, "types": [] } }, "apm": { "totalEvents": 0, "memRss": 0, - "apms": { - "total": 0 - }, + "apms": { "total": 0 }, "versions": [], - "config": { - "container": false - } + "config": { "container": false } }, "enterpriseSearch": { "clusterUuid": "lOF8kofiS_2DX58o9mXJ1Q", @@ -212,12 +141,7 @@ "versions": [] } }, - "alerts": { - "list": {}, - "alertsMeta": { - "enabled": true - } - }, + "alerts": { "list": {}, "alertsMeta": { "enabled": true } }, "isPrimary": false, "status": "yellow", "isCcrEnabled": true @@ -227,41 +151,21 @@ "cluster_uuid": "TkHOX_-1TzWwbROwQJU5IA", "cluster_name": "clusterone", "version": "7.0.0-alpha1", - "license": { - "status": "active", - "type": "trial", - "expiry_date_in_millis": 1505426327135 - }, + "license": { "status": "active", "type": "trial", "expiry_date_in_millis": 1505426327135 }, "elasticsearch": { "cluster_stats": { "indices": { "count": 5, - "docs": { - "deleted": 0, - "count": 150 - }, - "shards": { - "total": 26, - "primaries": 13 - }, - "store": { - "size_in_bytes": 4838464 - } + "docs": { "deleted": 0, "count": 150 }, + "shards": { "total": 26, "primaries": 13 }, + "store": { "size_in_bytes": 4838464 } }, "nodes": { - "fs": { - "total_in_bytes": 499065712640, - "available_in_bytes": 200404209664 - }, - "count": { - "total": 2 - }, + "fs": { "total_in_bytes": 499065712640, "available_in_bytes": 200404209664 }, + "count": { "total": 2 }, "jvm": { "max_uptime_in_millis": 741786, - "mem": { - "heap_max_in_bytes": 1257111552, - "heap_used_in_bytes": 465621856 - } + "mem": { "heap_max_in_bytes": 1257111552, "heap_used_in_bytes": 465621856 } } }, "status": "green" @@ -275,13 +179,8 @@ "avg_memory_used": 487782224, "max_uptime": 570039, "pipeline_count": 1, - "queue_types": { - "memory": 1, - "persisted": 0 - }, - "versions": [ - "7.0.0-alpha1" - ] + "queue_types": { "memory": 1, "persisted": 0 }, + "versions": ["7.0.0-alpha1"] }, "kibana": { "status": "green", @@ -291,26 +190,16 @@ "response_time_max": 1930, "memory_size": 231141376, "memory_limit": 1501560832, - "count": 1 - }, - "beats": { - "totalEvents": 0, - "bytesSent": 0, - "beats": { - "total": 0, - "types": [] - } + "count": 1, + "rules": { "cluster": null, "instance": null } }, + "beats": { "totalEvents": 0, "bytesSent": 0, "beats": { "total": 0, "types": [] } }, "apm": { "totalEvents": 0, "memRss": 0, - "apms": { - "total": 0 - }, + "apms": { "total": 0 }, "versions": [], - "config": { - "container": false - } + "config": { "container": false } }, "enterpriseSearch": { "clusterUuid": "TkHOX_-1TzWwbROwQJU5IA", @@ -326,14 +215,9 @@ "versions": [] } }, - "alerts": { - "list": {}, - "alertsMeta": { - "enabled": true - } - }, + "alerts": { "list": {}, "alertsMeta": { "enabled": true } }, "isPrimary": false, "status": "green", "isCcrEnabled": true } -] \ No newline at end of file +] diff --git a/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/overview.json b/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/overview.json index 143800c911307..f4bd6c33996ac 100644 --- a/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/overview.json +++ b/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/overview.json @@ -79,6 +79,7 @@ "requests_total": 914, "concurrent_connections": 646, "response_time_max": 2873, + "rules": { "cluster": null, "instance": null }, "memory_size": 196005888, "memory_limit": 1501560832, "count": 1 @@ -120,4 +121,4 @@ "status": "green", "isCcrEnabled": true } -] \ No newline at end of file +] diff --git a/x-pack/test/api_integration/apis/monitoring/common/mappings_exist.js b/x-pack/test/api_integration/apis/monitoring/common/mappings_exist.js index 22c7706b9a32f..9a18edf35fa79 100644 --- a/x-pack/test/api_integration/apis/monitoring/common/mappings_exist.js +++ b/x-pack/test/api_integration/apis/monitoring/common/mappings_exist.js @@ -56,6 +56,7 @@ export default function ({ getService }) { describe(`for ${name}`, () => { // eslint-disable-line no-loop-func for (const metric of Object.values(metrics)) { + if (metric.isNotSupportedInInternalCollection) continue; for (const field of metric.getFields()) { // eslint-disable-next-line no-loop-func it(`${field} should exist in the mappings`, () => { diff --git a/x-pack/test/api_integration/apis/monitoring/kibana/fixtures/instance.json b/x-pack/test/api_integration/apis/monitoring/kibana/fixtures/instance.json index b6f675ca1ad2e..b5a88fabdfa66 100644 --- a/x-pack/test/api_integration/apis/monitoring/kibana/fixtures/instance.json +++ b/x-pack/test/api_integration/apis/monitoring/kibana/fixtures/instance.json @@ -3,10 +3,7 @@ "kibana_os_load": [ { "bucket_size": "10 seconds", - "timeRange": { - "min": 1504027457000, - "max": 1504027568000 - }, + "timeRange": { "min": 1504027457000, "max": 1504027568000 }, "metric": { "app": "kibana", "field": "kibana_stats.os.load.1m", @@ -20,50 +17,20 @@ "isDerivative": false }, "data": [ - [ - 1504027460000, - 5.87109375 - ], - [ - 1504027470000, - 5.84375 - ], - [ - 1504027480000, - 5.08984375 - ], - [ - 1504027490000, - 4.53515625 - ], - [ - 1504027500000, - 3.990234375 - ], - [ - 1504027510000, - 3.537109375 - ], - [ - 1504027520000, - 3.29296875 - ], - [ - 1504027530000, - 3.2421875 - ], - [ - 1504027540000, - 3.19140625 - ] + [1504027460000, 5.87109375], + [1504027470000, 5.84375], + [1504027480000, 5.08984375], + [1504027490000, 4.53515625], + [1504027500000, 3.990234375], + [1504027510000, 3.537109375], + [1504027520000, 3.29296875], + [1504027530000, 3.2421875], + [1504027540000, 3.19140625] ] }, { "bucket_size": "10 seconds", - "timeRange": { - "min": 1504027457000, - "max": 1504027568000 - }, + "timeRange": { "min": 1504027457000, "max": 1504027568000 }, "metric": { "app": "kibana", "field": "kibana_stats.os.load.5m", @@ -77,50 +44,20 @@ "isDerivative": false }, "data": [ - [ - 1504027460000, - 4.92578125 - ], - [ - 1504027470000, - 4.9453125 - ], - [ - 1504027480000, - 4.81640625 - ], - [ - 1504027490000, - 4.70703125 - ], - [ - 1504027500000, - 4.58203125 - ], - [ - 1504027510000, - 4.46484375 - ], - [ - 1504027520000, - 4.3828125 - ], - [ - 1504027530000, - 4.3359375 - ], - [ - 1504027540000, - 4.29296875 - ] + [1504027460000, 4.92578125], + [1504027470000, 4.9453125], + [1504027480000, 4.81640625], + [1504027490000, 4.70703125], + [1504027500000, 4.58203125], + [1504027510000, 4.46484375], + [1504027520000, 4.3828125], + [1504027530000, 4.3359375], + [1504027540000, 4.29296875] ] }, { "bucket_size": "10 seconds", - "timeRange": { - "min": 1504027457000, - "max": 1504027568000 - }, + "timeRange": { "min": 1504027457000, "max": 1504027568000 }, "metric": { "app": "kibana", "field": "kibana_stats.os.load.15m", @@ -134,52 +71,22 @@ "isDerivative": false }, "data": [ - [ - 1504027460000, - 4.5390625 - ], - [ - 1504027470000, - 4.546875 - ], - [ - 1504027480000, - 4.5078125 - ], - [ - 1504027490000, - 4.46875 - ], - [ - 1504027500000, - 4.4296875 - ], - [ - 1504027510000, - 4.390625 - ], - [ - 1504027520000, - 4.359375 - ], - [ - 1504027530000, - 4.34375 - ], - [ - 1504027540000, - 4.328125 - ] + [1504027460000, 4.5390625], + [1504027470000, 4.546875], + [1504027480000, 4.5078125], + [1504027490000, 4.46875], + [1504027500000, 4.4296875], + [1504027510000, 4.390625], + [1504027520000, 4.359375], + [1504027530000, 4.34375], + [1504027540000, 4.328125] ] } ], "kibana_average_concurrent_connections": [ { "bucket_size": "10 seconds", - "timeRange": { - "min": 1504027457000, - "max": 1504027568000 - }, + "timeRange": { "min": 1504027457000, "max": 1504027568000 }, "metric": { "app": "kibana", "field": "kibana_stats.concurrent_connections", @@ -192,52 +99,22 @@ "isDerivative": false }, "data": [ - [ - 1504027460000, - 78 - ], - [ - 1504027470000, - 90 - ], - [ - 1504027480000, - 102 - ], - [ - 1504027490000, - 114 - ], - [ - 1504027500000, - 126 - ], - [ - 1504027510000, - 138 - ], - [ - 1504027520000, - 150 - ], - [ - 1504027530000, - 162 - ], - [ - 1504027540000, - 174 - ] + [1504027460000, 78], + [1504027470000, 90], + [1504027480000, 102], + [1504027490000, 114], + [1504027500000, 126], + [1504027510000, 138], + [1504027520000, 150], + [1504027530000, 162], + [1504027540000, 174] ] } ], "kibana_process_delay": [ { "bucket_size": "10 seconds", - "timeRange": { - "min": 1504027457000, - "max": 1504027568000 - }, + "timeRange": { "min": 1504027457000, "max": 1504027568000 }, "metric": { "app": "kibana", "field": "kibana_stats.process.event_loop_delay", @@ -250,52 +127,22 @@ "isDerivative": false }, "data": [ - [ - 1504027460000, - 9.576263427734375 - ], - [ - 1504027470000, - 10.395200729370117 - ], - [ - 1504027480000, - 11.072744369506836 - ], - [ - 1504027490000, - 11.617706298828125 - ], - [ - 1504027500000, - 12.510245323181152 - ], - [ - 1504027510000, - 13.343531608581543 - ], - [ - 1504027520000, - 14.059904098510742 - ], - [ - 1504027530000, - 14.816107749938965 - ], - [ - 1504027540000, - 15.663384437561035 - ] + [1504027460000, 9.576263427734375], + [1504027470000, 10.395200729370117], + [1504027480000, 11.072744369506836], + [1504027490000, 11.617706298828125], + [1504027500000, 12.510245323181152], + [1504027510000, 13.343531608581543], + [1504027520000, 14.059904098510742], + [1504027530000, 14.816107749938965], + [1504027540000, 15.663384437561035] ] } ], "kibana_memory": [ { "bucket_size": "10 seconds", - "timeRange": { - "min": 1504027457000, - "max": 1504027568000 - }, + "timeRange": { "min": 1504027457000, "max": 1504027568000 }, "metric": { "app": "kibana", "field": "kibana_stats.process.memory.heap.size_limit", @@ -309,50 +156,20 @@ "isDerivative": false }, "data": [ - [ - 1504027460000, - 1501560832 - ], - [ - 1504027470000, - 1501560832 - ], - [ - 1504027480000, - 1501560832 - ], - [ - 1504027490000, - 1501560832 - ], - [ - 1504027500000, - 1501560832 - ], - [ - 1504027510000, - 1501560832 - ], - [ - 1504027520000, - 1501560832 - ], - [ - 1504027530000, - 1501560832 - ], - [ - 1504027540000, - 1501560832 - ] + [1504027460000, 1501560832], + [1504027470000, 1501560832], + [1504027480000, 1501560832], + [1504027490000, 1501560832], + [1504027500000, 1501560832], + [1504027510000, 1501560832], + [1504027520000, 1501560832], + [1504027530000, 1501560832], + [1504027540000, 1501560832] ] }, { "bucket_size": "10 seconds", - "timeRange": { - "min": 1504027457000, - "max": 1504027568000 - }, + "timeRange": { "min": 1504027457000, "max": 1504027568000 }, "metric": { "app": "kibana", "field": "kibana_stats.process.memory.resident_set_size_in_bytes", @@ -366,52 +183,22 @@ "isDerivative": false }, "data": [ - [ - 1504027460000, - 228552704 - ], - [ - 1504027470000, - 231333888 - ], - [ - 1504027480000, - 232230912 - ], - [ - 1504027490000, - 229707776 - ], - [ - 1504027500000, - 230150144 - ], - [ - 1504027510000, - 230617088 - ], - [ - 1504027520000, - 229924864 - ], - [ - 1504027530000, - 230363136 - ], - [ - 1504027540000, - 230227968 - ] + [1504027460000, 228552704], + [1504027470000, 231333888], + [1504027480000, 232230912], + [1504027490000, 229707776], + [1504027500000, 230150144], + [1504027510000, 230617088], + [1504027520000, 229924864], + [1504027530000, 230363136], + [1504027540000, 230227968] ] } ], "kibana_response_times": [ { "bucket_size": "10 seconds", - "timeRange": { - "min": 1504027457000, - "max": 1504027568000 - }, + "timeRange": { "min": 1504027457000, "max": 1504027568000 }, "metric": { "app": "kibana", "field": "kibana_stats.response_times.max", @@ -425,50 +212,20 @@ "isDerivative": false }, "data": [ - [ - 1504027460000, - 2203 - ], - [ - 1504027470000, - 2203 - ], - [ - 1504027480000, - 2203 - ], - [ - 1504027490000, - 2203 - ], - [ - 1504027500000, - 2203 - ], - [ - 1504027510000, - 2203 - ], - [ - 1504027520000, - 2203 - ], - [ - 1504027530000, - 2203 - ], - [ - 1504027540000, - 2203 - ] + [1504027460000, 2203], + [1504027470000, 2203], + [1504027480000, 2203], + [1504027490000, 2203], + [1504027500000, 2203], + [1504027510000, 2203], + [1504027520000, 2203], + [1504027530000, 2203], + [1504027540000, 2203] ] }, { "bucket_size": "10 seconds", - "timeRange": { - "min": 1504027457000, - "max": 1504027568000 - }, + "timeRange": { "min": 1504027457000, "max": 1504027568000 }, "metric": { "app": "kibana", "field": "kibana_stats.response_times.average", @@ -482,52 +239,22 @@ "isDerivative": false }, "data": [ - [ - 1504027460000, - 171.05714416503906 - ], - [ - 1504027470000, - 171.05714416503906 - ], - [ - 1504027480000, - 171.05714416503906 - ], - [ - 1504027490000, - 171.05714416503906 - ], - [ - 1504027500000, - 171.05714416503906 - ], - [ - 1504027510000, - 171.05714416503906 - ], - [ - 1504027520000, - 171.05714416503906 - ], - [ - 1504027530000, - 171.05714416503906 - ], - [ - 1504027540000, - 171.05714416503906 - ] + [1504027460000, 171.05714416503906], + [1504027470000, 171.05714416503906], + [1504027480000, 171.05714416503906], + [1504027490000, 171.05714416503906], + [1504027500000, 171.05714416503906], + [1504027510000, 171.05714416503906], + [1504027520000, 171.05714416503906], + [1504027530000, 171.05714416503906], + [1504027540000, 171.05714416503906] ] } ], "kibana_requests": [ { "bucket_size": "10 seconds", - "timeRange": { - "min": 1504027457000, - "max": 1504027568000 - }, + "timeRange": { "min": 1504027457000, "max": 1504027568000 }, "metric": { "app": "kibana", "field": "kibana_stats.requests.total", @@ -540,50 +267,20 @@ "isDerivative": false }, "data": [ - [ - 1504027460000, - 113 - ], - [ - 1504027470000, - 146 - ], - [ - 1504027480000, - 162 - ], - [ - 1504027490000, - 164 - ], - [ - 1504027500000, - 166 - ], - [ - 1504027510000, - 168 - ], - [ - 1504027520000, - 170 - ], - [ - 1504027530000, - 172 - ], - [ - 1504027540000, - 174 - ] + [1504027460000, 113], + [1504027470000, 146], + [1504027480000, 162], + [1504027490000, 164], + [1504027500000, 166], + [1504027510000, 168], + [1504027520000, 170], + [1504027530000, 172], + [1504027540000, 174] ] }, { "bucket_size": "10 seconds", - "timeRange": { - "min": 1504027457000, - "max": 1504027568000 - }, + "timeRange": { "min": 1504027457000, "max": 1504027568000 }, "metric": { "app": "kibana", "field": "kibana_stats.requests.disconnects", @@ -596,42 +293,131 @@ "isDerivative": false }, "data": [ - [ - 1504027460000, - 0 - ], - [ - 1504027470000, - 0 - ], - [ - 1504027480000, - 0 - ], - [ - 1504027490000, - 0 - ], - [ - 1504027500000, - 0 - ], - [ - 1504027510000, - 0 - ], - [ - 1504027520000, - 0 - ], - [ - 1504027530000, - 0 - ], - [ - 1504027540000, - 0 - ] + [1504027460000, 0], + [1504027470000, 0], + [1504027480000, 0], + [1504027490000, 0], + [1504027500000, 0], + [1504027510000, 0], + [1504027520000, 0], + [1504027530000, 0], + [1504027540000, 0] + ] + } + ], + "kibana_instance_rule_failures": [ + { + "bucket_size": "10 seconds", + "timeRange": { "min": 1504027457000, "max": 1504027568000 }, + "metric": { + "app": "kibana", + "field": "kibana.node_rules.failures", + "metricAgg": "max", + "label": "Rule Failures Rate", + "description": "Rate of rule failures for the Kibana instance.", + "units": "", + "format": "0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "indices_found": { "internal": true, "metricbeat": false }, + "data": [ + [1504027460000, null], + [1504027470000, null], + [1504027480000, null], + [1504027490000, null], + [1504027500000, null], + [1504027510000, null], + [1504027520000, null], + [1504027530000, null], + [1504027540000, null] + ] + } + ], + "kibana_instance_rule_executions": [ + { + "bucket_size": "10 seconds", + "timeRange": { "min": 1504027457000, "max": 1504027568000 }, + "metric": { + "app": "kibana", + "field": "kibana.node_rules.executions", + "metricAgg": "max", + "label": "Rule Executions Rate", + "description": "Rate of rule executions for the Kibana instance.", + "units": "", + "format": "0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "indices_found": { "internal": true, "metricbeat": false }, + "data": [ + [1504027460000, null], + [1504027470000, null], + [1504027480000, null], + [1504027490000, null], + [1504027500000, null], + [1504027510000, null], + [1504027520000, null], + [1504027530000, null], + [1504027540000, null] + ] + } + ], + "kibana_instance_action_failures": [ + { + "bucket_size": "10 seconds", + "timeRange": { "min": 1504027457000, "max": 1504027568000 }, + "metric": { + "app": "kibana", + "field": "kibana.node_actions.failures", + "metricAgg": "max", + "label": "Action Failures Rate", + "description": "Rate of action failures for the Kibana instance.", + "units": "", + "format": "0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "indices_found": { "internal": true, "metricbeat": false }, + "data": [ + [1504027460000, null], + [1504027470000, null], + [1504027480000, null], + [1504027490000, null], + [1504027500000, null], + [1504027510000, null], + [1504027520000, null], + [1504027530000, null], + [1504027540000, null] + ] + } + ], + "kibana_instance_action_executions": [ + { + "bucket_size": "10 seconds", + "timeRange": { "min": 1504027457000, "max": 1504027568000 }, + "metric": { + "app": "kibana", + "field": "kibana.node_actions.executions", + "metricAgg": "max", + "label": "Action Executions Rate", + "description": "Rate of action executions for the Kibana instance.", + "units": "", + "format": "0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "indices_found": { "internal": true, "metricbeat": false }, + "data": [ + [1504027460000, null], + [1504027470000, null], + [1504027480000, null], + [1504027490000, null], + [1504027500000, null], + [1504027510000, null], + [1504027520000, null], + [1504027530000, null], + [1504027540000, null] ] } ] diff --git a/x-pack/test/api_integration/apis/monitoring/kibana/fixtures/overview.json b/x-pack/test/api_integration/apis/monitoring/kibana/fixtures/overview.json index d622eed75d3ec..9c6044f396d30 100644 --- a/x-pack/test/api_integration/apis/monitoring/kibana/fixtures/overview.json +++ b/x-pack/test/api_integration/apis/monitoring/kibana/fixtures/overview.json @@ -1,11 +1,13 @@ { "clusterStatus": { - "concurrent_connections": 174, - "count": 1, - "memory_limit": 1501560832, - "memory_size": 230227968, + "uuids": ["de3b8f2a-7bb9-4931-9bf3-997ba7824cf9"], + "status": "green", "requests_total": 174, + "concurrent_connections": 174, "response_time_max": 2203, + "memory_size": 230227968, + "memory_limit": 1501560832, + "count": 1, "status": "green", "some_status_is_stale": true, "uuids": [ @@ -16,175 +18,255 @@ "kibana_cluster_requests": [ { "bucket_size": "10 seconds", - "data": [ - [ - 1504027460000, - 113 - ], - [ - 1504027470000, - 146 - ], - [ - 1504027480000, - 162 - ], - [ - 1504027490000, - 164 - ], - [ - 1504027500000, - 166 - ], - [ - 1504027510000, - 168 - ], - [ - 1504027520000, - 170 - ], - [ - 1504027530000, - 172 - ], - [ - 1504027540000, - 174 - ] - ], + "timeRange": { "min": 1504027457000, "max": 1504027568000 }, "metric": { "app": "kibana", - "description": "Total number of client requests received by the Kibana instance.", "field": "kibana_stats.requests.total", + "metricAgg": "max", + "label": "Client Requests", + "description": "Total number of client requests received by the Kibana instance.", + "units": "", "format": "0.[00]", "hasCalculation": false, - "isDerivative": false, - "label": "Client Requests", - "metricAgg": "max", - "units": "" + "isDerivative": false }, - "timeRange": { - "max": 1504027568000, - "min": 1504027457000 - } + "data": [ + [1504027460000, 113], + [1504027470000, 146], + [1504027480000, 162], + [1504027490000, 164], + [1504027500000, 166], + [1504027510000, 168], + [1504027520000, 170], + [1504027530000, 172], + [1504027540000, 174] + ] } ], "kibana_cluster_response_times": [ { "bucket_size": "10 seconds", - "data": [ - [ - 1504027460000, - 2203 - ], - [ - 1504027470000, - 2203 - ], - [ - 1504027480000, - 2203 - ], - [ - 1504027490000, - 2203 - ], - [ - 1504027500000, - 2203 - ], - [ - 1504027510000, - 2203 - ], - [ - 1504027520000, - 2203 - ], - [ - 1504027530000, - 2203 - ], - [ - 1504027540000, - 2203 - ] - ], + "timeRange": { "min": 1504027457000, "max": 1504027568000 }, "metric": { "app": "kibana", - "description": "Maximum response time for client requests to the Kibana instance.", "field": "kibana_stats.response_times.max", + "metricAgg": "max", + "label": "Max", + "title": "Client Response Time", + "description": "Maximum response time for client requests to the Kibana instance.", + "units": "ms", "format": "0.[00]", "hasCalculation": false, - "isDerivative": false, - "label": "Max", + "isDerivative": false + }, + "data": [ + [1504027460000, 2203], + [1504027470000, 2203], + [1504027480000, 2203], + [1504027490000, 2203], + [1504027500000, 2203], + [1504027510000, 2203], + [1504027520000, 2203], + [1504027530000, 2203], + [1504027540000, 2203] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { "min": 1504027457000, "max": 1504027568000 }, + "metric": { + "app": "kibana", + "field": "kibana_stats.response_times.average", "metricAgg": "max", + "label": "Average", "title": "Client Response Time", - "units": "ms" + "description": "Average response time for client requests to the Kibana instance.", + "units": "ms", + "format": "0.[00]", + "hasCalculation": false, + "isDerivative": false }, - "timeRange": { - "max": 1504027568000, - "min": 1504027457000 - } + "data": [ + [1504027460000, 171.05714416503906], + [1504027470000, 171.05714416503906], + [1504027480000, 171.05714416503906], + [1504027490000, 171.05714416503906], + [1504027500000, 171.05714416503906], + [1504027510000, 171.05714416503906], + [1504027520000, 171.05714416503906], + [1504027530000, 171.05714416503906], + [1504027540000, 171.05714416503906] + ] + } + ], + "kibana_cluster_rule_overdue_count": [ + { + "bucket_size": "10 seconds", + "timeRange": { "min": 1504027457000, "max": 1504027568000 }, + "metric": { + "app": "kibana", + "field": "kibana.cluster_rules.overdue.count", + "metricAgg": "max", + "label": "Rule Overdue Count", + "description": "Number of overdue rules across the entire cluster.", + "units": "", + "format": "0.[00]", + "hasCalculation": false, + "isDerivative": false + }, + "indices_found": { "internal": true, "metricbeat": false }, + "data": [ + [1504027460000, null], + [1504027470000, null], + [1504027480000, null], + [1504027490000, null], + [1504027500000, null], + [1504027510000, null], + [1504027520000, null], + [1504027530000, null], + [1504027540000, null] + ] + } + ], + "kibana_cluster_rule_overdue_duration": [ + { + "bucket_size": "10 seconds", + "timeRange": { "min": 1504027457000, "max": 1504027568000 }, + "metric": { + "app": "kibana", + "field": "kibana.cluster_rules.overdue.delay.p50", + "metricAgg": "max", + "label": "Average Rule Overdue Delay", + "description": "Average delay of all overdue rules across the entire cluster.", + "units": "ms", + "format": "0.[00]", + "hasCalculation": false, + "isDerivative": false + }, + "indices_found": { "internal": true, "metricbeat": false }, + "data": [ + [1504027460000, null], + [1504027470000, null], + [1504027480000, null], + [1504027490000, null], + [1504027500000, null], + [1504027510000, null], + [1504027520000, null], + [1504027530000, null], + [1504027540000, null] + ] }, { "bucket_size": "10 seconds", + "timeRange": { "min": 1504027457000, "max": 1504027568000 }, + "metric": { + "app": "kibana", + "field": "kibana.cluster_rules.overdue.delay.p99", + "metricAgg": "max", + "label": "Worst Rule Overdue Delay", + "description": "Worst delay of all overdue rules across the entire cluster.", + "units": "ms", + "format": "0.[00]", + "hasCalculation": false, + "isDerivative": false + }, + "indices_found": { "internal": true, "metricbeat": false }, "data": [ - [ - 1504027460000, - 171.05714416503906 - ], - [ - 1504027470000, - 171.05714416503906 - ], - [ - 1504027480000, - 171.05714416503906 - ], - [ - 1504027490000, - 171.05714416503906 - ], - [ - 1504027500000, - 171.05714416503906 - ], - [ - 1504027510000, - 171.05714416503906 - ], - [ - 1504027520000, - 171.05714416503906 - ], - [ - 1504027530000, - 171.05714416503906 - ], - [ - 1504027540000, - 171.05714416503906 - ] - ], + [1504027460000, null], + [1504027470000, null], + [1504027480000, null], + [1504027490000, null], + [1504027500000, null], + [1504027510000, null], + [1504027520000, null], + [1504027530000, null], + [1504027540000, null] + ] + } + ], + "kibana_cluster_action_overdue_count": [ + { + "bucket_size": "10 seconds", + "timeRange": { "min": 1504027457000, "max": 1504027568000 }, "metric": { "app": "kibana", - "description": "Average response time for client requests to the Kibana instance.", - "field": "kibana_stats.response_times.average", + "field": "kibana.cluster_actions.overdue.count", + "metricAgg": "max", + "label": "Action Overdue Count", + "description": "Number of overdue actions across the entire cluster.", + "units": "", "format": "0.[00]", "hasCalculation": false, - "isDerivative": false, - "label": "Average", + "isDerivative": false + }, + "indices_found": { "internal": true, "metricbeat": false }, + "data": [ + [1504027460000, null], + [1504027470000, null], + [1504027480000, null], + [1504027490000, null], + [1504027500000, null], + [1504027510000, null], + [1504027520000, null], + [1504027530000, null], + [1504027540000, null] + ] + } + ], + "kibana_cluster_action_overdue_duration": [ + { + "bucket_size": "10 seconds", + "timeRange": { "min": 1504027457000, "max": 1504027568000 }, + "metric": { + "app": "kibana", + "field": "kibana.cluster_actions.overdue.delay.p50", "metricAgg": "max", - "title": "Client Response Time", - "units": "ms" + "label": "Average Action Overdue Delay", + "description": "Average delay of all overdue actions across the entire cluster.", + "units": "ms", + "format": "0.[00]", + "hasCalculation": false, + "isDerivative": false }, - "timeRange": { - "max": 1504027568000, - "min": 1504027457000 - } + "indices_found": { "internal": true, "metricbeat": false }, + "data": [ + [1504027460000, null], + [1504027470000, null], + [1504027480000, null], + [1504027490000, null], + [1504027500000, null], + [1504027510000, null], + [1504027520000, null], + [1504027530000, null], + [1504027540000, null] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { "min": 1504027457000, "max": 1504027568000 }, + "metric": { + "app": "kibana", + "field": "kibana.cluster_actions.overdue.delay.p99", + "metricAgg": "max", + "label": "Worst Action Overdue Delay", + "description": "Worst delay of all overdue actions across the entire cluster.", + "units": "ms", + "format": "0.[00]", + "hasCalculation": false, + "isDerivative": false + }, + "indices_found": { "internal": true, "metricbeat": false }, + "data": [ + [1504027460000, null], + [1504027470000, null], + [1504027480000, null], + [1504027490000, null], + [1504027500000, null], + [1504027510000, null], + [1504027520000, null], + [1504027530000, null], + [1504027540000, null] + ] } ] } diff --git a/x-pack/test/api_integration/apis/monitoring/kibana/index.js b/x-pack/test/api_integration/apis/monitoring/kibana/index.js index b54b09102bc1b..627cabc2ea99d 100644 --- a/x-pack/test/api_integration/apis/monitoring/kibana/index.js +++ b/x-pack/test/api_integration/apis/monitoring/kibana/index.js @@ -13,5 +13,7 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./listing_mb')); loadTestFile(require.resolve('./instance')); loadTestFile(require.resolve('./instance_mb')); + + loadTestFile(require.resolve('./rules_and_actions')); }); } diff --git a/x-pack/test/api_integration/apis/monitoring/kibana/instance.js b/x-pack/test/api_integration/apis/monitoring/kibana/instance.js index af7a360ec1bd8..5eef8051b6639 100644 --- a/x-pack/test/api_integration/apis/monitoring/kibana/instance.js +++ b/x-pack/test/api_integration/apis/monitoring/kibana/instance.js @@ -35,7 +35,6 @@ export default function ({ getService }) { .set('kbn-xsrf', 'xxx') .send({ timeRange }) .expect(200); - expect(body).to.eql(instanceFixture); }); }); diff --git a/x-pack/test/api_integration/apis/monitoring/kibana/instance_mb.js b/x-pack/test/api_integration/apis/monitoring/kibana/instance_mb.js index 8ad6f32c2c564..4aaa71101f43c 100644 --- a/x-pack/test/api_integration/apis/monitoring/kibana/instance_mb.js +++ b/x-pack/test/api_integration/apis/monitoring/kibana/instance_mb.js @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import { normalizeDataTypeDifferences } from '../normalize_data_type_differences'; +import { setIndicesFound } from '../set_indices_found'; import instanceFixture from './fixtures/instance.json'; import { getLifecycleMethods } from '../data_stream'; @@ -40,6 +41,7 @@ export default function ({ getService }) { .expect(200); body.metrics = normalizeDataTypeDifferences(body.metrics, instanceFixture); + instanceFixture.metrics = setIndicesFound(instanceFixture.metrics, true); expect(body).to.eql(instanceFixture); }); }); diff --git a/x-pack/test/api_integration/apis/monitoring/kibana/overview.js b/x-pack/test/api_integration/apis/monitoring/kibana/overview.js index 4887e7b62b246..4efdb8163cbdd 100644 --- a/x-pack/test/api_integration/apis/monitoring/kibana/overview.js +++ b/x-pack/test/api_integration/apis/monitoring/kibana/overview.js @@ -33,7 +33,6 @@ export default function ({ getService }) { .set('kbn-xsrf', 'xxx') .send({ timeRange }) .expect(200); - expect(body).to.eql(overviewFixture); }); }); diff --git a/x-pack/test/api_integration/apis/monitoring/kibana/overview_mb.js b/x-pack/test/api_integration/apis/monitoring/kibana/overview_mb.js index ca755c742e0b0..46edaa0d1ff13 100644 --- a/x-pack/test/api_integration/apis/monitoring/kibana/overview_mb.js +++ b/x-pack/test/api_integration/apis/monitoring/kibana/overview_mb.js @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import { normalizeDataTypeDifferences } from '../normalize_data_type_differences'; +import { setIndicesFound } from '../set_indices_found'; import overviewFixture from './fixtures/overview.json'; import { getLifecycleMethods } from '../data_stream'; @@ -38,6 +39,7 @@ export default function ({ getService }) { .expect(200); body.metrics = normalizeDataTypeDifferences(body.metrics, overviewFixture); + overviewFixture.metrics = setIndicesFound(overviewFixture.metrics, true); expect(body).to.eql(overviewFixture); }); }); diff --git a/x-pack/test/api_integration/apis/monitoring/kibana/rules_and_actions/fixtures/instance.json b/x-pack/test/api_integration/apis/monitoring/kibana/rules_and_actions/fixtures/instance.json new file mode 100644 index 0000000000000..17bde11cb7c4e --- /dev/null +++ b/x-pack/test/api_integration/apis/monitoring/kibana/rules_and_actions/fixtures/instance.json @@ -0,0 +1,334 @@ +{ + "metrics": { + "kibana_os_load": [ + { + "bucket_size": "1 min", + "timeRange": { "min": 1654022659267, "max": 1654027159267 }, + "metric": { + "app": "kibana", + "field": "kibana_stats.os.load.1m", + "metricAgg": "max", + "label": "1m", + "title": "System Load", + "description": "Load average over the last minute.", + "units": "", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1654023060000, 5.27734375], + [1654023120000, 4.78125] + ] + }, + { + "bucket_size": "1 min", + "timeRange": { "min": 1654022659267, "max": 1654027159267 }, + "metric": { + "app": "kibana", + "field": "kibana_stats.os.load.5m", + "metricAgg": "max", + "label": "5m", + "title": "System Load", + "description": "Load average over the last 5 minutes.", + "units": "", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1654023060000, 6.7734375], + [1654023120000, 6.5625] + ] + }, + { + "bucket_size": "1 min", + "timeRange": { "min": 1654022659267, "max": 1654027159267 }, + "metric": { + "app": "kibana", + "field": "kibana_stats.os.load.15m", + "metricAgg": "max", + "label": "15m", + "title": "System Load", + "description": "Load average over the last 15 minutes.", + "units": "", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1654023060000, 5.8359375], + [1654023120000, 5.78125] + ] + } + ], + "kibana_average_concurrent_connections": [ + { + "bucket_size": "1 min", + "timeRange": { "min": 1654022659267, "max": 1654027159267 }, + "metric": { + "app": "kibana", + "field": "kibana_stats.concurrent_connections", + "metricAgg": "max", + "label": "HTTP Connections", + "description": "Total number of open socket connections to the Kibana instance.", + "units": "", + "format": "0.[00]", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1654023060000, 10], + [1654023120000, 17] + ] + } + ], + "kibana_process_delay": [ + { + "bucket_size": "1 min", + "timeRange": { "min": 1654022659267, "max": 1654027159267 }, + "metric": { + "app": "kibana", + "field": "kibana_stats.process.event_loop_delay", + "metricAgg": "max", + "label": "Event Loop Delay", + "description": "Delay in Kibana server event loops. Longer delays may indicate blocking events in server thread, such as synchronous functions taking large amount of CPU time.", + "units": "ms", + "format": "0.[00]", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1654023060000, 10.624], + [1654023120000, 11.916] + ] + } + ], + "kibana_memory": [ + { + "bucket_size": "1 min", + "timeRange": { "min": 1654022659267, "max": 1654027159267 }, + "metric": { + "app": "kibana", + "field": "kibana_stats.process.memory.heap.size_limit", + "metricAgg": "max", + "label": "Heap Size Limit", + "title": "Memory Size", + "description": "Limit of memory usage before garbage collection.", + "units": "B", + "format": "0,0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1654023060000, 8438939648], + [1654023120000, 8438939648] + ] + }, + { + "bucket_size": "1 min", + "timeRange": { "min": 1654022659267, "max": 1654027159267 }, + "metric": { + "app": "kibana", + "field": "kibana_stats.process.memory.resident_set_size_in_bytes", + "metricAgg": "max", + "label": "Memory Size", + "title": "Memory Size", + "description": "Total heap used by Kibana running in Node.js.", + "units": "B", + "format": "0,0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1654023060000, 568397824], + [1654023120000, 632569856] + ] + } + ], + "kibana_response_times": [ + { + "bucket_size": "1 min", + "timeRange": { "min": 1654022659267, "max": 1654027159267 }, + "metric": { + "app": "kibana", + "field": "kibana_stats.response_times.max", + "metricAgg": "max", + "label": "Max", + "title": "Client Response Time", + "description": "Maximum response time for client requests to the Kibana instance.", + "units": "ms", + "format": "0.[00]", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1654023060000, 77], + [1654023120000, 418] + ] + }, + { + "bucket_size": "1 min", + "timeRange": { "min": 1654022659267, "max": 1654027159267 }, + "metric": { + "app": "kibana", + "field": "kibana_stats.response_times.average", + "metricAgg": "max", + "label": "Average", + "title": "Client Response Time", + "description": "Average response time for client requests to the Kibana instance.", + "units": "ms", + "format": "0.[00]", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1654023060000, 37], + [1654023120000, 40] + ] + } + ], + "kibana_requests": [ + { + "bucket_size": "1 min", + "timeRange": { "min": 1654022659267, "max": 1654027159267 }, + "metric": { + "app": "kibana", + "field": "kibana_stats.requests.total", + "metricAgg": "max", + "label": "Client Requests", + "description": "Total number of client requests received by the Kibana instance.", + "units": "", + "format": "0.[00]", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1654023060000, 3], + [1654023120000, 59] + ] + }, + { + "bucket_size": "1 min", + "timeRange": { "min": 1654022659267, "max": 1654027159267 }, + "metric": { + "app": "kibana", + "field": "kibana_stats.requests.disconnects", + "metricAgg": "max", + "label": "Client Disconnects", + "description": "Total number of client disconnects to the Kibana instance.", + "units": "", + "format": "0.[00]", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1654023060000, 0], + [1654023120000, 0] + ] + } + ], + "kibana_instance_rule_failures": [ + { + "bucket_size": "1 min", + "timeRange": { "min": 1654022659267, "max": 1654027159267 }, + "metric": { + "app": "kibana", + "field": "kibana.node_rules.failures", + "metricAgg": "max", + "label": "Rule Failures Rate", + "description": "Rate of rule failures for the Kibana instance.", + "units": "", + "format": "0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "indices_found": { "internal": false, "metricbeat": true }, + "data": [ + [1654023060000, null], + [1654023120000, 0] + ] + } + ], + "kibana_instance_rule_executions": [ + { + "bucket_size": "1 min", + "timeRange": { "min": 1654022659267, "max": 1654027159267 }, + "metric": { + "app": "kibana", + "field": "kibana.node_rules.executions", + "metricAgg": "max", + "label": "Rule Executions Rate", + "description": "Rate of rule executions for the Kibana instance.", + "units": "", + "format": "0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "indices_found": { "internal": false, "metricbeat": true }, + "data": [ + [1654023060000, null], + [1654023120000, 0.03333333333333333] + ] + } + ], + "kibana_instance_action_failures": [ + { + "bucket_size": "1 min", + "timeRange": { "min": 1654022659267, "max": 1654027159267 }, + "metric": { + "app": "kibana", + "field": "kibana.node_actions.failures", + "metricAgg": "max", + "label": "Action Failures Rate", + "description": "Rate of action failures for the Kibana instance.", + "units": "", + "format": "0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "indices_found": { "internal": false, "metricbeat": true }, + "data": [ + [1654023060000, null], + [1654023120000, 0] + ] + } + ], + "kibana_instance_action_executions": [ + { + "bucket_size": "1 min", + "timeRange": { "min": 1654022659267, "max": 1654027159267 }, + "metric": { + "app": "kibana", + "field": "kibana.node_actions.executions", + "metricAgg": "max", + "label": "Action Executions Rate", + "description": "Rate of action executions for the Kibana instance.", + "units": "", + "format": "0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "indices_found": { "internal": false, "metricbeat": true }, + "data": [ + [1654023060000, null], + [1654023120000, 0.03333333333333333] + ] + } + ] + }, + "kibanaSummary": { + "name": "CBR-MBP.local", + "host": "localhost", + "status": "green", + "statusIsStale": true, + "transport_address": "localhost:5601", + "uuid": "5b2de169-2785-441b-ae8c-186a1936b17d", + "snapshot": false, + "index": ".kibana", + "lastSeenTimestamp": "2022-05-31T18:52:10.179Z", + "version": "8.4.0", + "os_memory_free": 1504448512, + "uptime": 1845765 + } +} diff --git a/x-pack/test/api_integration/apis/monitoring/kibana/rules_and_actions/fixtures/overview.json b/x-pack/test/api_integration/apis/monitoring/kibana/rules_and_actions/fixtures/overview.json new file mode 100644 index 0000000000000..db7e4d7d39131 --- /dev/null +++ b/x-pack/test/api_integration/apis/monitoring/kibana/rules_and_actions/fixtures/overview.json @@ -0,0 +1,81 @@ +[ + { + "cluster_uuid": "SvjwrFv6Rvuqjm9-cSSVEg", + "cluster_name": "elasticsearch", + "version": "8.4.0", + "license": { "status": "active", "type": "basic" }, + "elasticsearch": { + "cluster_stats": { + "indices": { + "count": 13, + "docs": { "count": 2922 }, + "shards": { "total": 13, "primaries": 13 }, + "store": { "size_in_bytes": 10225568 } + }, + "nodes": { + "fs": { "total_in_bytes": 499963174912, "available_in_bytes": 85294256128 }, + "count": { "total": 1 }, + "jvm": { + "max_uptime_in_millis": 16567476, + "mem": { "heap_max_in_bytes": 1610612736, "heap_used_in_bytes": 984250168 } + } + }, + "status": "yellow" + }, + "logs": { + "enabled": false, + "types": [], + "reason": { + "indexPatternExists": false, + "indexPatternInTimeRangeExists": false, + "typeExistsAtAnyTime": false, + "typeExists": false, + "usingStructuredLogs": false, + "clusterExists": false, + "nodeExists": null, + "indexExists": null + } + } + }, + "logstash": {}, + "kibana": { + "status": "green", + "some_status_is_stale": true, + "requests_total": 59, + "concurrent_connections": 17, + "response_time_max": 418, + "memory_size": 632569856, + "memory_limit": 8438939648, + "count": 1, + "rules": { + "cluster": { "overdue": { "count": 1, "delay": { "p50": 1586, "p99": 1586 } } }, + "instance": { "failures": 0, "executions": 51, "timeouts": 0 } + } + }, + "beats": { "totalEvents": 0, "bytesSent": 0, "beats": { "total": 0, "types": [] } }, + "apm": { + "totalEvents": 0, + "memRss": 0, + "apms": { "total": 0 }, + "versions": [], + "config": { "container": false } + }, + "enterpriseSearch": { + "clusterUuid": "SvjwrFv6Rvuqjm9-cSSVEg", + "stats": { + "appSearchEngines": 0, + "workplaceSearchOrgSources": 0, + "workplaceSearchPrivateSources": 0, + "totalInstances": 0, + "uptime": 0, + "memUsed": 0, + "memCommitted": 0, + "memTotal": 0, + "versions": [] + } + }, + "isPrimary": true, + "status": "yellow", + "isCcrEnabled": false + } +] diff --git a/x-pack/test/api_integration/apis/monitoring/kibana/rules_and_actions/index.js b/x-pack/test/api_integration/apis/monitoring/kibana/rules_and_actions/index.js new file mode 100644 index 0000000000000..987418d01432d --- /dev/null +++ b/x-pack/test/api_integration/apis/monitoring/kibana/rules_and_actions/index.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export default function ({ loadTestFile }) { + describe('Rules and Actions', () => { + loadTestFile(require.resolve('./overview')); + loadTestFile(require.resolve('./instance')); + }); +} diff --git a/x-pack/test/api_integration/apis/monitoring/kibana/rules_and_actions/instance.js b/x-pack/test/api_integration/apis/monitoring/kibana/rules_and_actions/instance.js new file mode 100644 index 0000000000000..a16a053dde3a1 --- /dev/null +++ b/x-pack/test/api_integration/apis/monitoring/kibana/rules_and_actions/instance.js @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import fixture from './fixtures/instance.json'; +import { getLifecycleMethods } from '../../data_stream'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + const { setup, tearDown } = getLifecycleMethods(getService); + + describe('instance detail', () => { + const archive = 'x-pack/test/functional/es_archives/monitoring/kibana/rules_and_actions'; + const timeRange = { + min: '2022-05-31T18:44:19.267Z', + max: '2022-05-31T19:59:19.267Z', + }; + + before('load archive', () => { + return setup(archive); + }); + + after('unload archive', () => { + return tearDown(); + }); + + it('should get data for the kibana instance view', async () => { + const { body } = await supertest + .post( + '/api/monitoring/v1/clusters/SvjwrFv6Rvuqjm9-cSSVEg/kibana/5b2de169-2785-441b-ae8c-186a1936b17d' + ) + .set('kbn-xsrf', 'xxx') + .send({ timeRange }) + .expect(200); + expect(body).to.eql(fixture); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/monitoring/kibana/rules_and_actions/overview.js b/x-pack/test/api_integration/apis/monitoring/kibana/rules_and_actions/overview.js new file mode 100644 index 0000000000000..d6978dff4183c --- /dev/null +++ b/x-pack/test/api_integration/apis/monitoring/kibana/rules_and_actions/overview.js @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import fixture from './fixtures/overview.json'; +import { getLifecycleMethods } from '../../data_stream'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + const { setup, tearDown } = getLifecycleMethods(getService); + + describe('overview', () => { + const archive = 'x-pack/test/functional/es_archives/monitoring/kibana/rules_and_actions'; + const timeRange = { + min: '2022-05-31T18:44:19.267Z', + max: '2022-05-31T19:59:19.267Z', + }; + + before('load archive', () => { + return setup(archive); + }); + + after('unload archive', () => { + return tearDown(); + }); + + it('should get data for the entire cluster', async () => { + const { body } = await supertest + .post('/api/monitoring/v1/clusters/SvjwrFv6Rvuqjm9-cSSVEg') + .set('kbn-xsrf', 'xxx') + .send({ timeRange, codePaths: ['all'] }) + .expect(200); + expect(body).to.eql(fixture); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/monitoring/normalize_data_type_differences.ts b/x-pack/test/api_integration/apis/monitoring/normalize_data_type_differences.ts index ef27e439b74b2..6d6b7c2c36523 100644 --- a/x-pack/test/api_integration/apis/monitoring/normalize_data_type_differences.ts +++ b/x-pack/test/api_integration/apis/monitoring/normalize_data_type_differences.ts @@ -22,12 +22,15 @@ export function normalizeDataTypeDifferences(metrics: any, fixture: any) { return { ...item, data: item.data.map(([_x, y], index2) => { - const expectedY = fixture.metrics[metricName][index].data[index2][1]; - if (y !== expectedY) { - const normalizedY = numeral(y).format('0[.]00000'); - const normalizedExpectedY = numeral(y).format('0[.]00000'); - if (normalizedY === normalizedExpectedY) { - return [_x, expectedY]; + const data = fixture.metrics[metricName][index].data; + if (data.length) { + const expectedY = data[index2][1]; + if (y !== expectedY) { + const normalizedY = numeral(y).format('0[.]00000'); + const normalizedExpectedY = numeral(y).format('0[.]00000'); + if (normalizedY === normalizedExpectedY) { + return [_x, expectedY]; + } } } return [_x, y]; diff --git a/x-pack/test/api_integration/apis/monitoring/set_indices_found.tsx b/x-pack/test/api_integration/apis/monitoring/set_indices_found.tsx new file mode 100644 index 0000000000000..6992326cfc5d2 --- /dev/null +++ b/x-pack/test/api_integration/apis/monitoring/set_indices_found.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export function setIndicesFound(metrics: any, setFromMetricbeat: boolean = false) { + return Object.keys(metrics).reduce((accum: any, metricName) => { + accum[metricName] = metrics[metricName].map( + (item: { indices_found: { internal: boolean; metricbeat: boolean } }, index: number) => { + if (item.indices_found) { + return { + ...item, + indices_found: { + internal: !setFromMetricbeat, + metricbeat: setFromMetricbeat, + }, + }; + } + return item; + } + ); + return accum; + }, {}); +} diff --git a/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/clusters.json b/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/clusters.json index 2f0b8547d8192..3246ad2effb99 100644 --- a/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/clusters.json +++ b/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/clusters.json @@ -63,6 +63,7 @@ "requests_total": 42, "concurrent_connections": 0, "response_time_max": 864, + "rules": { "cluster": null, "instance": null }, "memory_size": 127283200, "memory_limit": 8564343808, "count": 1 @@ -153,6 +154,7 @@ "requests_total": 0, "concurrent_connections": 0, "response_time_max": 0, + "rules": { "cluster": null, "instance": null }, "memory_size": 0, "memory_limit": 0, "count": 0 @@ -204,4 +206,4 @@ "isPrimary": false, "isCcrEnabled": false } -] \ No newline at end of file +] diff --git a/x-pack/test/functional/es_archives/monitoring/kibana/rules_and_actions/data.json.gz b/x-pack/test/functional/es_archives/monitoring/kibana/rules_and_actions/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..138b385406b5df40bf27c91caec4a546f7440965 GIT binary patch literal 20462 zcmaglQ*dQ%+cxUBW4mM9>^L3Uw$)+B&Wvr_wr$%^$9B?jveFOU@BeDO|5|fn)EHHB zW1-G`Q>Pm=CY8cq2=SSmPLMsVYx4_*z={0qm{&d5yC7m$l2TC6T7LYL$%Mo% zw|#{d^=iWBW9XduzMl7pTK0|HQ3}2ueXH;x?es0InmyIs=Sgw73$|^$`;vuB*an=) z2$Nctu%G*YdFwU-zQdscYQZa5c)H#D*V~sS4cZ>cg-ol)aqYwY^L z!UvZpPaFBYx?SIXzq{Q(Wut(5nZEm}hyCoE$;M3&dN+Gt%G%ajj&!>QKk>oKUcD>Q zebB{*=o`4yY6e%gIezSu=fVtpCur7c=R67z%CoJ$8hWwedwP0vyZT+k@y_{v-BH%@ zYj5v#dqB(Xu+#Y(_d)eFwQAb@L(OH*xNQY?w>Ju6clvp+nZ1Ea`?8jYy)$L4XU+QYd6o*oCI{d<#wjv ziJDx9tzHCZX7%_HJ@?jp*P~)etNZ0*)zBxytkY{Xbrh)0tj%3J)dxxctygt1`Iw$> zFvmP%VahYce!Koc?i$K?AFgnT6&aVsN{Va5BceiU(_+hiV$jun9y5@*h>L8(o5w7z zV8I?VQH|+0Q9+*xZwm%ot98MJ=nq9knIy6}i@KO_vL>!^XUDI6eckIvUVya(Mb5dS zXV10!HC^1n-4N{7=id`;9`^`bUXC{nGu~yJACPXU7!C@i%Fb_ENCnqM#%?pA2H%o{ z^Eeykabyn;T+oGL8Cdwj=3C_**2OV&5R99{10{untC(?_F(cwyOE)NH5Ut)9u6bI% zIVn&c$yvOme&uR=LC| z*m+J0)?mhy9xn`jkA5xgJC@8*#U7IHh=Zt|6g)Ri2KndfR>x)?F2cZmD6(snqNh33zsm19Qyj3zB)ydg49$G8Q2&##0jSra*YV zkDXI$EwPk;e_M^0Dq=4RZ1&-QdnrDhG3@s#nHdkd$J|X z_+uQy`!~RC*85a^t0HaEvZ7{P0FAcFNCc^oYebCrv0`4bVn5CJj*ap1t;f+_+(0J) zptGE^?$(VEyYNSj;%1zPQIx<4x|c?X6m7 zX3Wc?(SSR^>T4Khvv1dDdsT<)FR7?qXMQK0+e0s!_g?+p^^vCJ%H6ZTs(wWDkY$`D z=biTUq1*NLeBSh7?O$Q5N8PQ`IHpp$dgwKEH_L$ewZWpClv&H*UKvlJr(jK!a}m8V zye1&*D_=87M|p5DiRGTtGXEq&0isBctWR!tu})m}FC0IjP+1~(Q8~o! zINV;8hXK7onTt(`P4&Dm*+}k7#rZb8S63HF9qXVB3St3DU?-3>uw|%f6g~0;Bl019 zp9~h-r6Cdow@}%!=cc?Vm(p>18E1IP>3Pw!=X7-T zu=EPJV=ySbFq+@%Nko5$&#<{b+x?Lna9wZ z68K8aujW$?qz~MyAm0n}B`1~sqh)`1gNJ1%j-$1|^XxZH><(|v&KEoPn1|Z84 zGD}Wh56W&0C&b6N`YH^e4wV-JvTp=HkRU$)Y>q}Cfb=1OKrCw4fU>Jz4``xww{X3= zU+#FG9~@7!26X}FI8y*lLNOnAXaW>|G9M|R6eZLY{z_DSGA>)$VAfr-Hkn77`UI zYzdz{_DG}A326B>eCmPBOOVZdm8khOoa_;6lPJ)tP_$i6d~maCaUV!;md z`rJSqc>$qg-V{uur(z)VyIyme>|XOD@Azh-+PRKfFn*`IoWfR3IuboKx;pMVeDKzd z@=Eu9?VRywor9h%+1t#UwrC2FUHpe0&B{bwZI#ri@c2{T$ebMpWZWW#xi`zsS= zw`yR**nM?WHm#%W0eS@Vnn+Qd;PVef3+~^uQ)UVlfkbcvCVL-gO$eZ2PC^2vSezgggGi_7z&6J=BPdkh#ty z5Q3~sc0fc<;v@&fz$tRyc>^KtBDw0N7#%AgBO#O}Sb_O+Qj2!ddmD+jH9pIjN7v*m ziuCPmnb-C0xawg4`q$o1pHAyz24czvq#=H7pH?78;1iJfg4p@}r~(gqoA#_NX94YL zU!-V$?MzfhDE9+E(!MioI~3r0c(`q(g3Zl%HBe_OVX~Wpc1}S1SQRF4}{g*E-dh2u!slJkv0pz zK1F9#^qOs|7@Fzq2Y1D$L~X@2mY6}I2`fa5?TMnizSWZ`fV|*Ag*(+qV9+m`#oA!` zo|_}ft!s6h?XwO%#mcaP1QU7uw0()e-4~!Gl@<5ISKWXUPKWt&A+( z!Fg8U;>0)cYtU~oj&#`D*PO*&f&4JxA)p6Z2AY8{!dgp;Zbk>_Yj8u!JPQFI2@5{ZWyb{oKNsj8-Y% zHp^GDs^MTwl)=YKx0|_?r?Ez$Rrb^*bbMX#(x!l_pI=@>fWymQU&jm#^Rb!jI8F3t zlq^O3=WZ3i9`nC`w2M8NUl{u<2GAtEKGR;(cUf(3Q!eg#vQk#k=+3}$ZJQZT9dwcq zKkx^ZoXUlst)^qKg28AhI2J&>PxU_Cb^<~jy95(|LzS3=p`WbH9>h`Hx7YYAQYuNX zwC!Fon^2RjZV#+DC9wuUjk7VVbz6bfNpG*17($wo<(z>95r&t5`GJCcfrLj)Hx;s_ znIi&9Uk%~`;lKu(j053_n&hjw%B|$1J(AFvuPoW6?n0Sjr|n{{Ka`T*dXr>Q)KIXl zFF&Gye{%T;%dQ`vonxlpu3tS#5OlkvqVgMA7^oQ+|H4*q@`d>g^pv*w}0ls zJF(bCjSI4|U8$U+*r;m%nP^hzMe;S>7=rOz>&%0VV-~aZyf?S z5%ks+n-b|g1xXFWa1!Gv7b`Dq{c*%KNOgkLXsKGC76R@EVf7UCj0lz7dF-ttAhsJz zH3y*v#W-3rw~0O2bZY<#E4C!(~~fQV1_wEOhfsM;)GtSEbi|kzWk;P-^kI z`&ta78L4)Rjz65XVd*=K5vQALOunpVngP|oyrMpu%hp56f8rObJ!6`zn9ti73 z7)J@sYcuWrn1C5?Ilk~>#C;g5d3(wkD! z+h{mX#oc+ti{aY+DDRxk51ssU@n*AKg|B95eD%>62=`(eRNB0jmLnu5$!r&J6@<-m zol8dn`VPyOj)9GE*W#*J@I(52^+wIyU#q{NI|?Fz$!}R;JtT`t}8Ae-t9oQ}rF6=$j97+_)AL<27n?@+8k9h@5BTkg_;`8%TfYbEsr5`P>e$V4fztTgPS$mueQCRadE$}MKxr3~+-3fKucEl?=_2PXruFox5cDmgF&x(`^T4KeL~} z*y54r9?9I1k%{TpgIi)_T?(fZKAhEdhmH{`;FTg*AO;S7w~IP2^@*`O0MnW5i5b z7b};re;i&$k;)9o(&)zuA2*T${j9eo*tL%EZ}1Lznz_14bGY2Lg1N*b`j+roe7p0n zr;Pg`hLMMm>?fVfb1!2BB zD;o)8VzR*w3c+0V3-W7So&wkQ_LJ!S_Ep%C7k)<sidqr)M2arC~#{QCTTYwwAt4L;Q4X zaBL{_L*zh5`N?`@F};vomX%OxefKIFas(V~hB!lzD~UXFYQDArRvd-g#2oVUFf$ex z_i!26n8(Cg=x-pGlFcMw>^BOQ0Co4f)$7+on~olf`2791^G!5h{~rWVGc7AD0b{$%Rs4EEVs2L zlQ{I*+0>QUC1ir%RiNdjv$F*R0DchQ9VevRNRYoMVg0bd?y+UyQ~JK#IS$;j5{D9V zEQ8#!9$8K=Wmjc&G}&e*LqzE#&Jh%jq0UW>SS|-s)AU$k&XW~O%cW(r`5y2-ejl$b zPW6V3t7`z>@LL6IFuxB_!=>&vr=BzqU|@!EK?c}E?8A;?#WPbJy6>NnT81x$(6#r| zBIDrm^8nmrRHrC+!BKw>pw%m>QR_|p`w`)w-BN3akcxE>?={ZDj_rY0vFe%44im=F z5@SG0TUJX~Xi#Cr2L;#wRuC(&wOB^X!w1g-IAMHXF|b5fLN|V5GK1h^JEf8arT(*E zB#dGP(ftG=0>}W8&If})A?xf&0bJ8>^xrQUO^M^N4*Vq%M+xEuC;)O0Ik1A9Bkb4N zSM5S$0*h>NQ`wofDAIdTU!&&}lIG)4IOM@dSj8-&=Lt%(Ogq`g-(W*l`S}1|bi#am zd9Xh)?Q)?^2^a+Q{JOz>p+HcQ&q+jW$y+hqCaU=$0Y)_(Y835H1^ZE4#QZG;!UCb+ z_F#3KIe@n-zHRU0NP_!gfN?>=~LY~EA>rP)$+y``SaJWx#t6# z+pXBs^rs3N&=okq3X|oRPV3!OS;N#UgV6V_o?oZQ(g9}acWCt#kxBik3L10nKV-$c zEc>4?_!_e{gXl&EjiLD z8-_;qJQ*ST_Jz&dUS_{e4P{WTH9hgst9-o(IHwBAWH)afE*A?CoNA@`$gsTkGw7>Y zfajWUajfOZ{pyt0b588e33~(r>Sny3{C;DIlNxAS+x@mqS?D!oY*F7Gn^BDwzUz8kX;wCJl=}c{azLpXN-SY+pw(?c4f1 z6q>H*FxO|g-yoW(i=Ojm?q3>Dn>ygfvqv{hncuJ1AEJ&|?&V*0m)h8Mqc%HeU=pP2 zBXJWDCsGno?NNZKfR#Xs_tdjv6P~J{i}&uo{E?f+FbNgGgfRos(D)yk`k@JsRU9vZ&3#! zRRfYGCK)zhia|iTSjLf^t8r#~{bxzX+0qJW(;0u!uKl8AF%4Xq9{sHZ#@N+=wkAm+h~$DB&Wh}{+Ku!-$fL{bQ7hDhR?nbJ0d3^kU?cJ_zOXux9J zU|s__u9E(`u1kJlz_g^qK??sVz%Y)~ttUc?QTZs0r~}!#7e3nIL(eeDezNqbdemuT zwEn)PV~RIcuJa&_eKcFCvX zOGDJ3ebj;Ktnll~alLfV-H?nmjc^VzjdH*JR ze08BcdpPgFF|thA@RUgA4WD2{3q3LUlX0KhxGh$a^^+IZ%GR=i$ZceKgL;xsgjacq*!02_x=qTTl_07iv60My^+S}pl zzXc^gs;{rl>{A#~qAeys(2TOWSEA_VR0bg;iAVnwrmj`nL4O8|B=Y-o*%%>gqLA(o z;|^o}Kxs9fcaM+R5Bswigu5ud%e}U_DSJse8Vxz(0N_uE1^&xs#aU~h_N;)dYK^U! zU;#Dq#Z8$s^jg93c(nZ##IBl)M$WE`PzU~cDo(8sxu{!dfZ$?uA*B}_qLKhh8T2BW z=0!U5bN*QS{@`0iAth9<6;2!f3vfAKMqPN#nt(ByS+tU2MGn7UNg5cT4j@nmR_XmA1;esmuiAoWrxbpmY|{=|(;J{G92 zt9MvJ$w_#e-S&MDRCNOtoLgxWgcYo1E{W(?;u4mm^y?!b0}DX`*ab%t+AfUviRs(T z_&EX$-7rER`*P@=A4vS&s+q{TuH&r?ebnrt-t~O#?tHw*S?eWf7mr^r@Z8pVskKI* zv<`e!fvSLXfZIV_3ItvD5ofD`bXItE%$-{|jkywKn((#=lnBdTJ(6Faz~Id)e(!6U z;ss5ru4aH6o*hl@{OpdlTtuUx$i3iHg(lO|mnGb#4Zre)hud)vQm*cg6^LEKtxXsz zhi7iIzduiNemcQ-dOg{*%==L3_&~a=q-*R`3Fi@DX|4p)1a1Ve0$-L8UV+Y625zi1 zZru5NkeTiL`|h1`L;+JAZ~Y?&N3-l(p^DvDQF(-z!Z~VVasMk-<+1W~n1Y)?&2|&U zBN)V8;P9tRd_GX+1F@)^;wBTu{a4HVL5MO+0&D~_3>FRbM|07H$)Xj&qerY|z*CJ? z7(Xz{R?GUQGiRHMZ(rZ34Gp(<=E0Kc2Z}!zE7RdoFGSTN=h7$F$yI8X`MFIxM45y;K)es3KrR<^xF2;t2}@V%&qVD$VuC@QzUv8f zq#ti4OeL)ZX%K1p#!$pfnGCZeMHxzYCZ#g2Hy!XxViRa1&We&@Z_J05h1~&q>jM*C z0u?BU983$ZL1R36?4@pSf%JLi8Z1ci@{IlqeT^> z^{DV0_1fIF7G}>6o`J|mD-+fJFD`TZhs(_VaG8cdSibH&pGahssPQn_nh*rjj+_54nKq-T*vpIT=Gla`}bz9_v^#5OM$~ynijT- z%XpNpErkY~z#x0r!>rMk1WTpVODczbrrArKzB-^CDm&;a9x`n~RM)XlQ<9eKkbq&2 zxAW3*iP0#l{B^{D!DgTG_9hWh65&&4xT7`6bv zUu{HynJ9NX#cwK8ZLgGoT7uzJeT{q}I<_KP8>~`R3qx2)EKC40$lxs>kfR525Hs&ag1v_V+o!w)LTOKgoti-!N zE*;K!etHH= z)|2xUXdnbwOfWBZSFSp=S#&sUkl%*Ng_C@5g}tTnN<@|deEJw#juxbFPdmsh^f)y` zRd>7F#R1xv&x11BWK~yB2&u9nQ;n8}IrpU!S0= z;nV+$IbI5#tlVekT$;^%gHk?R6R!kv&CP~?-}U_Bq0?Fb%2`n;mB@&aw98ADT4iOC z&qFWlBinjC6&9a$pktEw)Av7GR`a)(i4V~JpIWy2ua>p5Td*9EDyv%bf6&D8J#Y=T z>Iz7{_|M~jzySI_u?+EFEMxe@vOX;ZGQ0P%x^p_qRpjI!dX2s-5GHB8brXY#lTv~! zQ1ImOX0*P)fO>#G#q9725ofF(5~zsm0rHm_5Qi-EFYa$2{k>tW@AcW#obqjjkyWoU z+*|dplhc_Dtey|_;v(kaEi<*bc`UzJ8iHS)hCXjL7W@O0h4FI!@o`v-*!~54%o94= zE);VbFrg8Jy+&{)>F<$2t*KkySuYSt=g;A@Z=;e1<|ZeiwLR*&Gg>bij63rnb^iJ0 z2<2a9L|Zk#z5>PEg4BFrNo2U>CK z7{>dC8^tpxKCCyVs)|z8iU3*@=ZSpSnynDx@KjmIH1%mm3$s`W)+@l6?OFu$#LFj= z#S+#-Da7mUYRMh5ozfPTPh4P2K-M!_^*8+B-!g5Jep@e396p1rj5`v9-pC!$ORi~< zWXeizG83qmgAHj2tNndc$4xB`*WUI!WAE$c#tTL)=4B=3ZAy%@W+-l_7hRCUjG&(M z=&CtoXfsl7Y@5_Z3f0@oRHbx|E|~ePTT$FbTCTro0raoE-yxJgx=gIE`RQjXj|v?j zH5Jvk0$hhHTqlIpjR&AjlM)FE*8dBUiT!d(nrX2v;yvL+K=&V{bb+i>)vmw~N7l6I zz#ct&h~fF}?QK=nC$q=?^*F7D6=#WuD;f-GJRFPIwddyki#&sV>P0}h($enhX~ZVN zMA3cTaBG!U3{j*)Z8aT&UeU2Dq8IxvO+%LvPet*G7j#xMMPJb!_}4AP2*Sf2e~MLh zCC0g6{Rn+gG$*CMHd%d<7TlCrj>k{ZN%b$h`x| zznGeZjAh6$9KKiMmcMiJT!lkRxdx}{)L{kUp%7hpU`>U^`uCa&>1~d}<7axxos4*t z1=zqfods6IB>{8Yr8Pb`XLae)aU_ioa^Q88L<1MuU&T{rO9&{rMvn%Tu#H`sOtN#F zBld2`-ZR9lUYGPY6m)%%)8`22@f_V3+I$e=5%&=ANNnpfsFC-Nq;7`Mo6Bs3LEqH4 zDAbUPUy(c9^&^e)00c(ouiQ_FXHN`_gl3;y;#*r%ToiFJHGkD^zF`#s+j2L=&U5y*bn=2EXZl|q zE#xd&L`|=2k7dlav&!CRI0`<9$tWUh5tKZRRkA^Po^-6P#w>vHa}_Hqg=9nQOmpn= zqb8_|n{6g&4K)RJcCa7|2YGzF)dm9AM5PCb&EzoOskiSo>ZETQW9E(5&D&u(cgb`! zN#o*-DTd~+h503Wgm*Uv^D9r=8MmLGJjUsxs3(mUSdP!n;cE-B?Sby^(5UeP-~J+9 zUuX06{u00MV5`VG{Yf`%iT3(!BgmWX#K~AAO5dby?w)Rq@kh)3Bh_Geo}2e6BI*(+ zCqp1L|1>$MKwrF3FeOiIqT90tw7fLyE;_5$`dPhT0TAyz;GaL0{lhc zsB#l%suP$r@-(R3s1&0Q8^L6%SCzHAB{X2~Q@wtE{4nQ~^YOdMmq2z#H%m@do$2 zc63?xyJhRP!zki9-2N(-WSXE_Ko8K}2z54o4FcV|1C4E1Lh=P14jLPkg~IF)&z(-% zWA?KInOYj|#&zp$M$*;{mtFq{qE@x>@B$&HmqX{&N`q%%e=;2zO^;;936KM1GShW1 zFwM~%J7n6N!I`j(n1=VZl4L8=z7l4~Qdh=ei7548P5qBIKKYUj1v#*`9_V6h15GoT znM{wbn0-zh`~s@l0+te}dwD?r^KHsze$Lst2N#11!1+C2Pj>%{jO-v0-RjQSRRX$z zP6*Gpv%Hg=eiorc-((0z?ZRX)nHfhd<_UsU`2UTJlaEXvn1ha6KtEwDQ05C4Fbo{a zfr=_PiqMjg3IsU+MMekGgj=|w$NvdlUiKbQQ|QTc|HeBez@cB8Fj=k>uLy{)&-d&g zIB8hLV4l#Qu{xj5jg;QPU+uR;jELg}po3q2L{e*jZnKvfDg6$|cR>uTm`urel_SU?o9ijh1=equMPBHGiN??^za>Vpz7e%wd z(0-I^LM`|7of)tUmsWs4>|d0|7`DohSx?(x)Csy>U@(piZ%TDx#}uLq+iUB}n7Y|{ zH+XAzIQsj?`&8IMpu2wCXwIQDHXReSB9mDy-Kh7WMo0GYmE~U%!UC(PqreR_+m~S- zc+l2vA02gp!md}$ph7|%Dr>hKWBv-xf7#6b|HWpv|FT)}KWxVMe`T}Hf7t9>!GG9n zc~sTQ0e!gYGLkRT^BM2$rS)ERx2i{2z27L;^{gMvp^{@3(F5+UAuZ0-S4!oe?}RvR z4dGAr#tI{)zoP`@-!m(h10H?T)XV&7caowZ2oksxI4Vo{elQ4~%zE*H;@LLfLLp%GRdZ>d2bUHZ4m|IzIc>Uv6 z;1<&M1bvJ%ZYSqE)-`*MTMOf+r(eXAV zE+?ezaEy(;u3psxK(`Y4-)cTn#Ec*J;m}80tG*KFiCei!*+ALmWHX21m?`3RI zbnRhDO#pgLKN@IeVc>sKE#L%_tb~&IpU#kYE($b(F99CIhxi2g&qkK*=8M1{!gTD0 z$oqQ7e~X#D%7Y<;P>V4`{u?wy6DEzO0hb&3P3*mzQR%!A88BBd#E!WYS9jbJ*idlG zRt{Ab-cis%Wd(9=pU*r5T_W)vRdK}{Q}~lBkAvkIgw>jJQKg}4;?sMYF&I7_P6reX zo*&$2^z-M2Ez$CPIneHW4#MsF_OyD`rF`iP)=_p&qKKr1906PcqyU@`A_g8U!5TA) zOQHyzUu&En>yKoIso|P}YHNH(qqTm~Lwao7kiL*(;i@1MQb|!6!z0DPm4_-B zeFH}1rl(W|BWnxKO_})v3X~hzrFPN4_A>H@?P?NR+rxg(H7j@X{?}BWW-X~Rs$h)) z&i@|O-Tv38UIOamGx}R7rurj5fG`kTy9;x(XjwTDLy z>I6cWrej7#nZs_6N+>mD2CE`@h5%+Jp&h9eAIeOBJV{WKVx))ml`>y)8zg!xPLM82 zoz7%>|4n`O1o_P@1D4nG@%8=kbSCO{YO4uc8`T(fwgBiS@Da!~c($Z)4otoz@KLSt zk-_}%LN!aXpkz~>QD=g3N`}>E)=~2_>lpo4*0Ja_>-bYXXE>z*kwjZQbT5D(+7m=v zQP1PjF+B=G73~+(hM}hY1c&5Q3r@EUDkUkZ?NO!xF=^#d&5=Dh10{V2m3muwz zWa^-jM0rz;bO49m-;02fhcqGv<_9jbQMO#iqFlA;q5fpyTti?;I^%Yv&dC>|Mu@Pl z=U~v=JGbM5Zz_;iNk#p-cufZ5kW#1T3+GdPaZTh|eZR&+h>+O*M%aU>SS28H@=S z`1O3cfqYBId+hI)F$@=@Sx)jF3bE~D~h-_ea zT|yv+5)wBQut?$2$YdGPd6>P;E;M0;Cc|nH5d38L!+XC6CTNJ6N#T(~juGVwm{ z?XF0Pa-V*)Jk}Se5s_gQK}W+9VDU5g7y*qap^Wf@VDK}oIfs5`Ip;j+Qg}+iwvuOm zu*6zsE0dE5FxJv@+BG?F+`{wdhigaS;_7SZgABiG4JH;y-quyVfIN+g~?*{EF%y1>yO9N|}@m(W1y8h9s&aNtPh^s-5 zxxTx^nD&4OYjxm;k(mC!$GC({C($yen&9@phhd^I2|-@ zzaZ9se~YyDW-crj7ZE)WK3ardEt6LtE3$3SuVaGkhibe+C=&LR%%F7yJD~ zU3!%)%(B{gQyvn=g>e)@l&HRg z@Y$+PtbR@5PnaPEqa_n=U)fi9NZOKC1?iy_>Est&OK%}_-Dr!5Ni;<@4G)8Zy$G;+ znG^+BqM@FkUDV#-{Vt9AVYYQn-O?pNQK^bOn43u z3Q#$S?D1P)Cb+ToW;+f;_JD_wO^+9#JBXtRr#Hh)YdoDnb1yO(&CaVuEOvsXlsnF824n6LX! zQ;5E(W72(+j@L?xKX8%T)P$X~0+Ph^NgA{Ht$o&kT7d0A zFIWr~ZY-2VnGSisx(w(>43aw^-QA8oZ&&9{e#qVJZ4_`78s_Ud6#Guw?_)H6E1FQ3|R7AAF^*-VfGY@`E%%x}5VjBFh#2KCno|CSC zk)loJ`aE(xwma9yZY+KxPFm<%Y}i|(M^GZ50LX_Bf5RcUAN5f46n{JadS>T`qhk|Q zm%M!XJm*&#T>EzvOP2UEiUo_yk{i}*t*-8pD*E?H4KUq3_zXXGuIB-!(!on1BnDE>t%Q6HxGSpz*0%h zNTm6%FG<^tUriBHlMqA)bY<%K(`*B$s_f{3du1w`o0&5XL)k!tn zwJT^N(IBWQ>voz#15yf-lDe^24JV2XG(%B`!Q|=>FlB!N)7&3mO39UBmi!=VlBF?! zVRf+l;uhm1%wEB_tkJtlKK|W$!jf*3idu!UfHc_d0iX~j5s=KSqz5HCFg zaz9|UDeVR-1w9_#cwM<`$c?-9E`rz6GWZ!K;T@$j4S~}C@fP1M9d!6$YaZfMkI(M35v{d zqkiSt+Q78~da0`ZfW8PeZYHigByDazex+dVNvYfX&*+-kg|`45o8$uvjs?wx`g%`w zQg!&yql<92It+k;>WI#ax(BBN-2t5q4=YK%-J07wlP+}b*2c8j3EF{ zTzED{!QJPAyA3^ZShvsQin_)FY$ozSl=LCIK$J8N2hqJvOzcN@`ANPiDd4)Spd$NL>j3m#56bLbmG{FC9h{Rdc#*yK8 zf}R<{H5rBFq{gI@^GJ^Maee{0w1H1VUGtDXTdN(X!Mt?YompEC7Z8HUckf+6w;5uw zwJ}3$n~VHJx}2EXyvgYUU7lm|zHJUGmZHhITSee9LwWK$e#HBVM7yoo75a2SlIGk4 zwijt>!#U%J;~>i~@EA^SY5hDNZpGmfjn`lD7INK|o%Z9W9(#)4n6*4tth_iMg3k5s z4@+|R!3(+jrK?t8zehnKxpamX35yGEGZQh`EHJG({T7I-Hqq>(HC0>_Ibe3b@iQ-r zdU-PM8owdJ5+&Ue8G)>wf~s?dtCxjpn1X3+3S6^cL+YUkA&{xt2$FQ_krEm*EnP@_ zf6u}JY$B8c-Tei0Bs~Og{Kr8vdKpOMmTvT&`J5X+DKHPSu;-ae2Gy;?B%cSzTectO zMe`R1hJ*8OuPIo?6=TtEx{F`Ooan9A5u&h-<1NJ}NUAL8nJ(JQldriX4OQKbUd-7^ zrQN14Qm{{@O1sdPyeBv*2WqMQf3j=T1Tj?q?~R8EXEX>8orh;kVOD69T%`w{I~^b8QDhM)%D7I+ z^WFMNmRR;Dy%vQXWpgYlLO8w>T9`Cw@By^+nQU$x{0Hp#9Zk@^nDrRkKf_~L#!e|_ ze?70F->zsntK&;yhbs{pasG%O7lP}B6c~vI`3ZG|JWZNCcIZQA8n*D;ft9`f>Iu_J7ye6qI4TF?$qd;_+&9EfGob|ecrn0V{Dlp6=YTjp z#W2uM9B@0ozfn%{S28PrksY~p(`QaufEVB)1BFx*g@HIT#V>?nTnPIoBqksP`1Bk& zqGuf;fHlMhY%8ZCJ3z@xMj?eVlCXeL%s6_KAVGi%pv0!Y31RwudMG@L5Q;0Fx0C6Azb>Mu#ftUa4GX^VoBSAkyr9<9&g}m6h9Dv@xrU6Am zO3M>KDjI&c*OnK2L9>gyk7GwEG3h?U(%WuJ_f1^}o_P=|E`OGiwg!2WwuCf6Mx8dr zY1~rWKT)u`u#p8IfHrEK!p{Fd)I|#W6i@guY?Z`9zd=eqaF{H8*k+{0KR8Ane|;lg zHUK--&S$Bmor_-)*HeU$we{i2^KAG2^x|Z%W6>YI_|pp>;;#t^h96_t?a9NL$ne=< zd@pAS_65KomKX*I=uP{pilafwRP77J{e_JI<4Q4R8|)=>N^eV%s-NEv0QLn69vSC1 zkANRv|3rCa6T~z2XN2u^Yh#iO>{`q7x*W?Gxt}@M6mg0mQ-E7lf!q5_kpN{Z~`lW4}Dcc2E@h`ozqCK+5 zVl+btBb`m*-KOV4N4JwMi@0CCZ7!}{kF~Vy0yHETh-pxN=VNS9{CP%CpVI?i(_*^~ z^%R-l@O$Wt7MuyOV#ScfJmGFI_ZWu_mjlWRxjX%|#pF6PHv?EYj&o|;^EDLO6rjGw z`^D5cQ->>LySAbAHV)JkoI`)MUNn;0we`saq+o1tgY8|I*>3uR=V`6vk%MSQj|oqV zFXfR3P#xCum8{d^XIY~{7SqiRg~*cS6s(e%Y?3bq*hpbtV}DcLGNt_bGsQVmkow|| zlGlHKYDO~@vHWihm{oMro@7H?st9}Ksp2`oMtLP(Pdq1KFRDCQaB0h_`A!CFS0(tm7T$h zBCJgRbH-?kBW=7vc=;0eH(g*P51EbGLz9$SO%Y@yb^26ixPs@zzWLUx1Z+6CaLVA~ zQ0G%+6!j}LrhUn_wFCO;_|b=|Z+gh5J$_Gb%5V{|iw}DVGU-Jzw1R+^CCzqz_GLsY zGq!BYP$dA%tL48IWY_HP@^F~J4@rTPfQv!oz;WKFpp^-hk+Vk=b3(T+-Nawq1m-Op z=uK+#Xkryw3j*w6X_c_J%9>XcW3|ZqRk3pj*nRx8fS~QcD2{|F|ES&?%+Js4+xO*s zmv9o0&)|zuhNB*YbmvW49lw{S^Xv85+{4k3Wms9O!H)eiR=vAn8(Q zX^ua8YXM|)a~LR72O=q6d@%+&im5$nE*X zw3lc94Kf{&NfaZ4`W0IWA;y4-t6CGe5?Tmm@oOQ5Uv@x{V|PRO{Hd3|{OtcP_0o!r zjKLCqL|m|6V4v?p0U}ohCzqsDuI%eM#q~jn{7hv4$z-B({FTZO{wI|Y{3n%xZh*29 z20a{nU9->aqo!oM$*uqtATM6)z!AIyqe6I{xl z!jfSo?3vzdk40hN{}h%YuKy`4)m%bfxJPGyOi=KX{b;8b-&96H&V(kyc-p2)rrE4a zG4)s>i(nD7I0$SevEjwVT~zVL2}k~Bvmq}2y9>-T_)nCfMf2xp=z_ARHp_U_v8Mil zrqsC3WyGZ(|JN#2kTXx(g!y1QC8 zt0HQf2Iid9Qu8q63eQ)IvTx_GnR*El%eetM|Yt!WKh|L%EPkMC^>8H}Z_PVKe; zDT696GzD$>+URPxyQsLSU){!Dm5cEI)iW9&Q2{eqxMe6aaL~46DC;f7N46|sO@P;E%~{lCW0QT-g&d;vW63~ywzdDl_bgN zhG>L+J}eE2sv7_8ua z!6(y@pS;mc|KX^98`rG`>Hs&+%CLl*ZpjvC^GOXdHfrUyDPV4(pP~Raop$)$=Ch^L zowgeF7P8$B{c9r5k3DLV=ii!=%H021Q!3(?-g=U>m(iI0QD44H0mmkNPWYdiQYa?LFdg~%8?MY*FGW1XKROJuT#lzqu^h2N;|y}xsQ|2%&^%lCZG`+3fJ zKF{m5V&_CLQ$grIX7AldnExP^?hdouY5}6h6%%<2NCTU%KieJo`xjs2%WZt=1XjDc&)LG@6qGbi;tjG4K zr7PX&O%PAa?mO`tzJUA#U+mG~i&pJD{x6U{wO&R;DuItb{R29TllG!`R&9o&p1;a7 zerzb2yN5E2n1+Z(9ve#61!^k+xIPMz_?La;l*6;kB8G`^yu}8`>dFCoS5lZ=@?LT$ z5!80lSwD-|;3cX!Tq`%@l{%y(VOJmS=- zp0QetQ;}o8kgzEmDJ$rXIzKwNvD2}wy#$GRnf8wLDmid`lOFQs zv6(nH{1|){=yYOoHiCtKxtk09;0MyrT z#==CU1Q!LqZTPk0CXej&H+ErC!MQC}jy3;iGCq=Gos~jK?kV58lOhnEvZJ;gHX=6n zh-4@d%LrjN5{}jKeePg|7uB9(&VoL*StG4hyT`+ynbs*XT!9c{??Lm z|4&N-{i7wJkF_NH`Y$c%h@^g-ajE;4mUPm3>^#7@G!Ar+Lp}2{9<^u9RzPvHRyV72 zxUI4IC&}BG>tt5)1Kfa~3$r3gLO4XCj?_Feh;n8DSZ(6KgXn01 z#7{|!RbOT&a|(A9e77S5EZnwM0(ziq(-KBs(ri>sD>kCHxnR~qNn4YG;6|IYJKz9b z->q_AWbA_f*R9z(bNRw0W7K|oh($y5N3nLA#LS(pC0<7?I#9lN!dKEO2CDN}CEY?O z;g;{-f&O-NW(RubMvhoInEGZb{W1pUu+wvw)Gedg(!LZvv=fbD=3hOjf9KYRgY}Ut z*`U3?x;O7C=X&LNx`Kp4wfk>^c3xF6Co#F9Sp1ftMya{&2&6jqXh!knfZIgd`9p*j)ZhE{ zU4DJBW7F+Gq`ocFF5Mv= z((TLk&1qQFq{07m2Jr>rce(HpAkd$3vrQniO3LUXl%Qx%r?|(e`!GLXqC?i>Tv%Va zk#SP`&C+!@i`U0x&g`FMPAlbXTA9=5h+5vTq0W2#srbKjPQQy5GD?P5*j0E~Ul?48 z&1X7(px0vgMw1%gLR@ys!A)PT7HxC{V7mkK8rP$u`uLiHVX~GdcvMD&U5ddcbl9f4 z>!JoOnJz`TDW+Q8rkcUDkntaJSHDVeVE`ge!tH0SB}CJi{|IS8qLj)HH&PYz)ip54 zr!&PETVVwoSE3ubC3C!aHZIjm7diF^N+CSj!&bqu3j3A&sA@9cXq8Nv3^xh>-n0$x z7F70I#-91?veJ_jP+o!3_Lhx;W}~%6MA8x?(R4aHw^fWI>{-wI%JkM;ubd!Or%o8Z z^8?#{-!ZcdoJ#bdl;Xl})z`u;lzR;vFtv0ZjFi#{icjDfRB1x^)mrc1U6|>Jfamp{ zfC?v&u){gqzmQcCgxQtga#7}yl#A4cOXeV!_*V%gpZ5U+Q$wFE<5l&s_THg5Bkq(w z($IxLgJO&YJJ4T?kqc?R%c8x+ZH!c%(&wDGS{^{ z^2-eDFqL$UHi3?HD4uPvn#S-N)NPNXuceGu0ofIDy=TVQSK9=iuul?|iSk3sZ-1C@ z2}ao0;J;6E`_$b?m}@4^TI&eykZ%X&KAml~kdf2t^}qLrq|`(WeI?8Dj~kI2EP>5$ z1`@5hRD!vRBonIYn5J7AzGKDPu`^|!R^`RBSaZVLh>~V_Dwi4Wm)4BTq^01!+WjA6 zd>z-MwjNBA?@fe+zo-wS;-YyB-!DL{n$jpfQVFVSn08%glAHvOkzU2sWx}n!bPm}R zDDjiXoXDN#k^yV>Vl&-614W8Nv402F(|wnQZ#}oh>b;xro*{>-_zM>H2!17aqN`44 z+^88KZ@}A%L^>7yg4*8j>OO5#Ff@@g<+mf+zr2_Wbs;A=CZRlaZ++B2I8)Nr)%Hhm z>9BlNVippH!H!X$Y_=5{J8u6tRD`zqYZbc4Pw(8r1w6@8x_UngkW^!kF(AGN`-oJNb(&h&sSeM?zBT8fCD}Bhy|^d#mk-wTX5;o2tG(D_4^I453{wUir-oPPrSO zQHkj!R2V~XT{tlSFSf@;J$94>t#aMiV3rpG&Ad>y>(nI^pG%FHFPz)@-)(g>beQ{4k%7a2)leerV$9V!-`D zFt`+BO;2DVA-HuJNr-*v;p}RMz3x~%mK%=wXSdl^p4SD4hO^CnsBMg~{Ew%gZX2(S ziL_?;xe&eunfEr_x^+oY1h!Tvkn+kPo09P!808Z8=u+ZU1soI1)KqnlA&XznQ2HV1 z52=!+HDH|PV-pbLzHZGLFr*onL~gEbjWd-gJ)>v%!l2l}G(ie>4aD%=oqZY+gGKkp zXA0@v)5=<4eE_@mHgtDrqk>zHaT>9h-F*PqNn5w^{5&a0k=Xe)!SAWj^^=nnue8M_)-xq)unl@%+%(LG?S|GUgBe1e*GwDJ{4C@GP z{klr){d#9O=gB;3)DPZvq^ZP|2|c&(g<*qo@DJWr82HDKQ)1nrQ-Yf2ZJ$r;2w|1N z+Xf|=EA$0SmpHd&M^5I!X`lGb$JCK?+jr!2?=vcLmlu83v|wrp_}=FDKmzX$$+zII z@bx|$7?T6wJYm4zVv?2jIwGbW|GD*)t;ao>s3jHxd0b1z$3KFU%(=xsL+xfL(wQnH zFp}YRD{H{scnC6a<7D1rI`8{y>L>F~C;AXfy803Bk^P7eo_>UN<++Oh00efiY=Lle N9#^h+7dy^;;=hJwKt=!n literal 0 HcmV?d00001 From e6882edf960e18e3cb2c11253360d0d4b181d3af Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Thu, 9 Jun 2022 16:57:02 -0400 Subject: [PATCH 03/62] Update Saved Object Management docs (#134031) --- .../images/management-saved-objects.png | Bin 98367 -> 189398 bytes .../managing-saved-objects.asciidoc | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/management/images/management-saved-objects.png b/docs/management/images/management-saved-objects.png index 0ee720cfdb39d7610d4e105e2f743cd70b70007a..fc61b92841e0e86c1db769febe175218f9998a4a 100644 GIT binary patch literal 189398 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

CkWWZ3f%AjXfI57eSLNg1%ov%` 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

##### Modal with overflow:

##### Steps to test: Since this modal will only show if you have one of the affected v1/v2 jobs, and these jobs were removed in 8.3, the easiest way to exercise this codepath is to just create a custom ML job using one of the affected job names, e.g. `v2_rare_process_by_host_linux_ecs`. Otherwise you would need to load an 8.2 instance, install the jobs, then go through an upgrade to 8.3 (which the ML folks have already tested, so no need to verify that part of the flow again). Since we can only add this check client-side if the user has ML privileges, when testing with a user _without_ ML privileges, you should expect _not to see_ a confirmation modal.
Create Custom ML Job Instructions * Go to ML App and click Jobs/Create Anomaly_Detection_Jobs_-_Machine_Learning_-_Elastic * Select Security Solution Data View Anomaly_Detection_Jobs_-_Machine_Learning_-_Elastic * Select Single Metric Anomaly_Detection_Jobs_-_Machine_Learning_-_Elastic Click next Pick a field Click next Input `v2_rare_process_by_host_linux_ecs` as the job id Then `security` for the group Click next Click next Click create Job Go back to the Security Rules Management page
### Checklist Delete any items that are not applicable to this PR. - [X] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [X] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials * Collaborating with @elastic/security-docs folks in dedicated docs issue: https://github.com/elastic/security-docs/issues/1748 --- .../affected_job_ids.ts | 93 ++++++++++--------- .../index.test.tsx | 2 +- .../translations.tsx | 18 ++-- .../modals/ml_job_upgrade_modal/index.tsx | 66 +++++++++++++ .../ml_job_upgrade_modal/translations.tsx | 70 ++++++++++++++ .../pre_packaged_rules/load_empty_prompt.tsx | 42 ++++++++- .../pages/detection_engine/rules/index.tsx | 57 ++++++++++-- 7 files changed, 284 insertions(+), 64 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detections/components/modals/ml_job_upgrade_modal/index.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/modals/ml_job_upgrade_modal/translations.tsx diff --git a/x-pack/plugins/security_solution/public/detections/components/callouts/ml_job_compatibility_callout/affected_job_ids.ts b/x-pack/plugins/security_solution/public/detections/components/callouts/ml_job_compatibility_callout/affected_job_ids.ts index 8228a5c314c2d..eef5595d688ee 100644 --- a/x-pack/plugins/security_solution/public/detections/components/callouts/ml_job_compatibility_callout/affected_job_ids.ts +++ b/x-pack/plugins/security_solution/public/detections/components/callouts/ml_job_compatibility_callout/affected_job_ids.ts @@ -13,51 +13,60 @@ // the IDs from those modules (as found in e.g. // x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/manifest.json) // allows us to make this determination from a single API call. +// +// Note: In 8.3 the V3 ML modules were released (#131166) and a large portion of the V1/V2 +// jobs were removed or replaced. We'll use this same list of affectedJobIds to show a modal +// before updating their Detection Rules to inform users of this change and any actions that +// must be taken before updating to ensure continued functionality if they still need to run +// the V1/V2 jobs +// For details see: https://github.com/elastic/kibana/issues/128121 export const affectedJobIds: string[] = [ // security_linux module - 'v2_rare_process_by_host_linux_ecs', - 'v2_linux_rare_metadata_user', - 'v2_linux_rare_metadata_process', - 'v2_linux_anomalous_user_name_ecs', - 'v2_linux_anomalous_process_all_hosts_ecs', - 'v2_linux_anomalous_network_port_activity_ecs', + 'v2_rare_process_by_host_linux_ecs', // Replaced by: v3_rare_process_by_host_linux_ecs + 'v2_linux_rare_metadata_user', // Replaced by: v3_linux_rare_metadata_user + 'v2_linux_rare_metadata_process', // Replaced by: v3_linux_rare_metadata_process + 'v2_linux_anomalous_user_name_ecs', // Replaced by: v3_linux_anomalous_user_name_ecs + 'v2_linux_anomalous_process_all_hosts_ecs', // Replaced by: v3_linux_anomalous_process_all_hosts_ecs + 'v2_linux_anomalous_network_port_activity_ecs', // Replaced by: v3_linux_anomalous_network_port_activity_ecs // security_windows module - 'v2_rare_process_by_host_windows_ecs', - 'v2_windows_anomalous_network_activity_ecs', - 'v2_windows_anomalous_path_activity_ecs', - 'v2_windows_anomalous_process_all_hosts_ecs', - 'v2_windows_anomalous_process_creation', - 'v2_windows_anomalous_user_name_ecs', - 'v2_windows_rare_metadata_process', - 'v2_windows_rare_metadata_user', + 'v2_rare_process_by_host_windows_ecs', // Replaced by: v3_rare_process_by_host_windows_ecs + 'v2_windows_anomalous_network_activity_ecs', // Replaced by: v3_windows_anomalous_network_activity_ecs + 'v2_windows_anomalous_path_activity_ecs', // Replaced by: v3_windows_anomalous_path_activity_ecs + 'v2_windows_anomalous_process_all_hosts_ecs', // Replaced by: v3_windows_anomalous_process_all_hosts_ecs + 'v2_windows_anomalous_process_creation', // Replaced by: v3_windows_anomalous_process_creation + 'v2_windows_anomalous_user_name_ecs', // Replaced by: v3_windows_anomalous_user_name_ecs + 'v2_windows_rare_metadata_process', // Replaced by: v3_windows_rare_metadata_process + 'v2_windows_rare_metadata_user', // Replaced by: v3_windows_rare_metadata_user // siem_auditbeat module - 'rare_process_by_host_linux_ecs', - 'linux_anomalous_network_activity_ecs', - 'linux_anomalous_network_port_activity_ecs', - 'linux_anomalous_network_service', - 'linux_anomalous_network_url_activity_ecs', - 'linux_anomalous_process_all_hosts_ecs', - 'linux_anomalous_user_name_ecs', - 'linux_rare_metadata_process', - 'linux_rare_metadata_user', - 'linux_rare_user_compiler', - 'linux_rare_kernel_module_arguments', - 'linux_rare_sudo_user', - 'linux_system_user_discovery', - 'linux_system_information_discovery', - 'linux_system_process_discovery', - 'linux_network_connection_discovery', - 'linux_network_configuration_discovery', + 'rare_process_by_host_linux_ecs', // Replaced by: v3_rare_process_by_host_linux_ecs + 'linux_anomalous_network_activity_ecs', // Replaced by: v3_linux_anomalous_network_activity_ecs + 'linux_anomalous_network_port_activity_ecs', // Replaced by: v3_linux_anomalous_network_port_activity_ecs + 'linux_anomalous_network_service', // Deleted + 'linux_anomalous_network_url_activity_ecs', // Deleted + 'linux_anomalous_process_all_hosts_ecs', // Replaced by: v3_linux_anomalous_process_all_hosts_ecs + 'linux_anomalous_user_name_ecs', // Replaced by: v3_linux_anomalous_user_name_ecs + 'linux_rare_metadata_process', // Replaced by: v3_linux_rare_metadata_process + 'linux_rare_metadata_user', // Replaced by: v3_linux_rare_metadata_user + 'linux_rare_user_compiler', // Replaced by: v3_linux_rare_user_compiler + 'linux_rare_kernel_module_arguments', // Deleted + 'linux_rare_sudo_user', // Replaced by: v3_linux_rare_sudo_user + 'linux_system_user_discovery', // Replaced by: v3_linux_system_user_discovery + 'linux_system_information_discovery', // Replaced by: v3_linux_system_information_discovery + 'linux_system_process_discovery', // Replaced by: v3_linux_system_process_discovery + 'linux_network_connection_discovery', // Replaced by: v3_linux_network_connection_discovery + 'linux_network_configuration_discovery', // Replaced by: v3_linux_network_configuration_discovery // siem_winlogbeat module - 'rare_process_by_host_windows_ecs', - 'windows_anomalous_network_activity_ecs', - 'windows_anomalous_path_activity_ecs', - 'windows_anomalous_process_all_hosts_ecs', - 'windows_anomalous_process_creation', - 'windows_anomalous_script', - 'windows_anomalous_service', - 'windows_anomalous_user_name_ecs', - 'windows_rare_user_runas_event', - 'windows_rare_metadata_process', - 'windows_rare_metadata_user', + 'rare_process_by_host_windows_ecs', // Replaced by: v3_rare_process_by_host_windows_ecs + 'windows_anomalous_network_activity_ecs', // Replaced by: v3_windows_anomalous_network_activity_ecs + 'windows_anomalous_path_activity_ecs', // Replaced by: v3_windows_anomalous_path_activity_ecs + 'windows_anomalous_process_all_hosts_ecs', // Replaced by: v3_windows_anomalous_process_all_hosts_ecs + 'windows_anomalous_process_creation', // Replaced by: v3_windows_anomalous_process_creation + 'windows_anomalous_script', // Replaced by: v3_windows_anomalous_script + 'windows_anomalous_service', // Replaced by: v3_windows_anomalous_service + 'windows_anomalous_user_name_ecs', // Replaced by: v3_windows_anomalous_user_name_ecs + 'windows_rare_user_runas_event', // Replaced by: v3_windows_rare_user_runas_event + 'windows_rare_metadata_process', // Replaced by: v3_windows_rare_metadata_process + 'windows_rare_metadata_user', // Replaced by: v3_windows_rare_metadata_user + // siem_winlogbeat_auth module + 'windows_rare_user_type10_remote_login', // Replaced by: v3_windows_rare_user_type10_remote_login ]; diff --git a/x-pack/plugins/security_solution/public/detections/components/callouts/ml_job_compatibility_callout/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/callouts/ml_job_compatibility_callout/index.test.tsx index 889f19acae630..f593f9e12d1b7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/callouts/ml_job_compatibility_callout/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/callouts/ml_job_compatibility_callout/index.test.tsx @@ -44,7 +44,7 @@ describe('MlJobCompatibilityCallout', () => { it('does not render if no affected jobs are installed', () => { (useInstalledSecurityJobs as jest.Mock).mockReturnValue({ loading: false, - jobs: [{ id: 'windows_rare_user_type10_remote_login' }], + jobs: [{ id: 'high_count_network_denies' }], }); const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/detections/components/callouts/ml_job_compatibility_callout/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/callouts/ml_job_compatibility_callout/translations.tsx index dcc52c4656bd6..b3fc174688089 100644 --- a/x-pack/plugins/security_solution/public/detections/components/callouts/ml_job_compatibility_callout/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/callouts/ml_job_compatibility_callout/translations.tsx @@ -26,15 +26,15 @@ export const MlJobCompatibilityCalloutBody = () => (

), diff --git a/x-pack/plugins/security_solution/public/detections/components/modals/ml_job_upgrade_modal/index.tsx b/x-pack/plugins/security_solution/public/detections/components/modals/ml_job_upgrade_modal/index.tsx new file mode 100644 index 0000000000000..6506080a3da15 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/modals/ml_job_upgrade_modal/index.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiConfirmModal } from '@elastic/eui'; +import { MlSummaryJob } from '@kbn/ml-plugin/common'; +import React, { memo } from 'react'; +import styled from 'styled-components'; +import { rgba } from 'polished'; +import * as i18n from './translations'; + +const JobsUL = styled.ul` + max-height: 200px; + overflow-y: auto; + + &::-webkit-scrollbar { + height: ${({ theme }) => theme.eui.euiScrollBar}; + width: ${({ theme }) => theme.eui.euiScrollBar}; + } + + &::-webkit-scrollbar-thumb { + background-clip: content-box; + background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)}; + border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent; + } + + &::-webkit-scrollbar-corner, + &::-webkit-scrollbar-track { + background-color: transparent; + } +`; + +export interface MlJobUpgradeModalProps { + jobs: MlSummaryJob[]; + onCancel: ( + event?: React.KeyboardEvent | React.MouseEvent + ) => void; + onConfirm?: (event: React.MouseEvent) => void; +} + +const MlJobUpgradeModalComponent = ({ jobs, onCancel, onConfirm }: MlJobUpgradeModalProps) => { + return ( + + + {i18n.ML_JOB_UPGRADE_MODAL_AFFECTED_JOBS} + + {jobs.map((j) => { + return
  • {j.id}
  • ; + })} +
    +
    + ); +}; + +export const MlJobUpgradeModal = memo(MlJobUpgradeModalComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/modals/ml_job_upgrade_modal/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/modals/ml_job_upgrade_modal/translations.tsx new file mode 100644 index 0000000000000..8163eca279cf0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/modals/ml_job_upgrade_modal/translations.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { MlJobCompatibilityLink } from '../../../../common/components/links_to_docs'; + +export const ML_JOB_UPGRADE_MODAL_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.mlJobUpgradeModal.messageTitle', + { + defaultMessage: 'ML rule updates may override your existing rules', + } +); + +export const ML_JOB_UPGRADE_MODAL_CANCEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.mlJobUpgradeModal.cancelTitle', + { + defaultMessage: 'Cancel', + } +); + +export const ML_JOB_UPGRADE_MODAL_CONFIRM = i18n.translate( + 'xpack.securitySolution.detectionEngine.mlJobUpgradeModal.confirmTitle', + { + defaultMessage: 'Load rules', + } +); + +export const ML_JOB_UPGRADE_MODAL_AFFECTED_JOBS = i18n.translate( + 'xpack.securitySolution.detectionEngine.mlJobUpgradeModal.affectedJobsTitle', + { + defaultMessage: 'Affected jobs:', + } +); + +export const MlJobUpgradeModalBody = () => ( + + +

    + ), + docs: ( +
      +
    • + +
    • +
    + ), + }} + /> +); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx index a633f5f76a610..252787c9dd853 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx @@ -6,8 +6,11 @@ */ import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React, { memo, useMemo } from 'react'; +import React, { memo, useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; +import { affectedJobIds } from '../../callouts/ml_job_compatibility_callout/affected_job_ids'; +import { MlJobUpgradeModal } from '../../modals/ml_job_upgrade_modal'; +import { useInstalledSecurityJobs } from '../../../../common/components/ml/hooks/use_installed_security_jobs'; import * as i18n from './translations'; import { SecuritySolutionLinkButton } from '../../../../common/components/links'; @@ -35,6 +38,10 @@ const PrePackagedRulesPromptComponent: React.FC = ( const [{ isSignalIndexExists, isAuthenticated, hasEncryptionKey, canUserCRUD, hasIndexWrite }] = useUserData(); + const { loading: loadingJobs, jobs } = useInstalledSecurityJobs(); + const legacyJobsInstalled = jobs.filter((job) => affectedJobIds.includes(job.id)); + const [isUpgradeModalVisible, setIsUpgradeModalVisible] = useState(false); + const { getLoadPrebuiltRulesAndTemplatesButton } = usePrePackagedRules({ canUserCRUD, hasIndexWrite, @@ -43,15 +50,35 @@ const PrePackagedRulesPromptComponent: React.FC = ( hasEncryptionKey, }); + // Wrapper to add confirmation modal for users who may be running older ML Jobs that would + // be overridden by updating their rules. For details, see: https://github.com/elastic/kibana/issues/128121 + const mlJobUpgradeModalConfirm = useCallback(() => { + setIsUpgradeModalVisible(false); + createPrePackagedRules(); + }, [createPrePackagedRules, setIsUpgradeModalVisible]); + const loadPrebuiltRulesAndTemplatesButton = useMemo( () => getLoadPrebuiltRulesAndTemplatesButton({ - isDisabled: !userHasPermissions || loading, - onClick: createPrePackagedRules, + isDisabled: !userHasPermissions || loading || loadingJobs, + onClick: () => { + if (legacyJobsInstalled.length > 0) { + setIsUpgradeModalVisible(true); + } else { + createPrePackagedRules(); + } + }, fill: true, 'data-test-subj': 'load-prebuilt-rules', }), - [getLoadPrebuiltRulesAndTemplatesButton, createPrePackagedRules, userHasPermissions, loading] + [ + getLoadPrebuiltRulesAndTemplatesButton, + userHasPermissions, + loading, + loadingJobs, + legacyJobsInstalled, + createPrePackagedRules, + ] ); return ( @@ -71,6 +98,13 @@ const PrePackagedRulesPromptComponent: React.FC = ( {i18n.CREATE_RULE_ACTION} + {isUpgradeModalVisible && ( + setIsUpgradeModalVisible(false)} + onConfirm={mlJobUpgradeModalConfirm} + /> + )} } /> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx index 9afc9086caa65..edb46c7dffe51 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx @@ -6,7 +6,10 @@ */ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; +import { MlJobUpgradeModal } from '../../../components/modals/ml_job_upgrade_modal'; +import { affectedJobIds } from '../../../components/callouts/ml_job_compatibility_callout/affected_job_ids'; +import { useInstalledSecurityJobs } from '../../../../common/components/ml/hooks/use_installed_security_jobs'; import { usePrePackagedRules, importRules } from '../../../containers/detection_engine/rules'; import { useListsConfig } from '../../../containers/detection_engine/lists/use_lists_config'; @@ -47,6 +50,10 @@ const RulesPageComponent: React.FC = () => { const { startTransaction } = useStartTransaction(); const invalidateRules = useInvalidateRules(); + const { loading: loadingJobs, jobs } = useInstalledSecurityJobs(); + const legacyJobsInstalled = jobs.filter((job) => affectedJobIds.includes(job.id)); + const [isUpgradeModalVisible, setIsUpgradeModalVisible] = useState(false); + const [ { loading: userInfoLoading, @@ -103,6 +110,21 @@ const RulesPageComponent: React.FC = () => { } }, [createPrePackagedRules, invalidateRules, startTransaction]); + // Wrapper to add confirmation modal for users who may be running older ML Jobs that would + // be overridden by updating their rules. For details, see: https://github.com/elastic/kibana/issues/128121 + const mlJobUpgradeModalConfirm = useCallback(async () => { + setIsUpgradeModalVisible(false); + await handleCreatePrePackagedRules(); + }, [handleCreatePrePackagedRules, setIsUpgradeModalVisible]); + + const showMlJobUpgradeModal = useCallback(async () => { + if (legacyJobsInstalled.length > 0) { + setIsUpgradeModalVisible(true); + } else { + await handleCreatePrePackagedRules(); + } + }, [handleCreatePrePackagedRules, legacyJobsInstalled.length]); + const handleRefetchPrePackagedRulesStatus = useCallback(() => { if (refetchPrePackagedRulesStatus != null) { return refetchPrePackagedRulesStatus(); @@ -114,19 +136,31 @@ const RulesPageComponent: React.FC = () => { const loadPrebuiltRulesAndTemplatesButton = useMemo( () => getLoadPrebuiltRulesAndTemplatesButton({ - isDisabled: !userHasPermissions(canUserCRUD) || loading, - onClick: handleCreatePrePackagedRules, + isDisabled: !userHasPermissions(canUserCRUD) || loading || loadingJobs, + onClick: showMlJobUpgradeModal, }), - [canUserCRUD, getLoadPrebuiltRulesAndTemplatesButton, handleCreatePrePackagedRules, loading] + [ + canUserCRUD, + getLoadPrebuiltRulesAndTemplatesButton, + showMlJobUpgradeModal, + loading, + loadingJobs, + ] ); const reloadPrebuiltRulesAndTemplatesButton = useMemo( () => getReloadPrebuiltRulesAndTemplatesButton({ - isDisabled: !userHasPermissions(canUserCRUD) || loading, - onClick: handleCreatePrePackagedRules, + isDisabled: !userHasPermissions(canUserCRUD) || loading || loadingJobs, + onClick: showMlJobUpgradeModal, }), - [canUserCRUD, getReloadPrebuiltRulesAndTemplatesButton, handleCreatePrePackagedRules, loading] + [ + canUserCRUD, + getReloadPrebuiltRulesAndTemplatesButton, + showMlJobUpgradeModal, + loading, + loadingJobs, + ] ); if ( @@ -149,6 +183,13 @@ const RulesPageComponent: React.FC = () => { + {isUpgradeModalVisible && ( + setIsUpgradeModalVisible(false)} + onConfirm={mlJobUpgradeModalConfirm} + /> + )} { loading={loadingCreatePrePackagedRules} numberOfUpdatedRules={rulesNotUpdated ?? 0} numberOfUpdatedTimelines={timelinesNotUpdated ?? 0} - updateRules={handleCreatePrePackagedRules} + updateRules={showMlJobUpgradeModal} /> )} Date: Thu, 9 Jun 2022 21:23:51 -0300 Subject: [PATCH 08/62] [Discover] Fixed tooltips for text and keyword fields displaying 'Unknown field' in expanded document (#133536) * [Discover] Fixed tooltips for text and keyword fields displaying 'Unknown field' * [Discover] Added tests for getFieldTypeName function * [Discover] Fixing issue where i18n defaultMessage was a variable instead of a constant string Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/sidebar/discover_field.tsx | 2 +- .../components/field_name/field_name.tsx | 2 +- .../components/field_name/field_type_name.ts | 63 ------------------- .../public/utils/get_field_type_name.test.ts | 34 ++++++++++ .../lib => utils}/get_field_type_name.ts | 60 ++++++++++++------ 5 files changed, 77 insertions(+), 84 deletions(-) delete mode 100644 src/plugins/discover/public/components/field_name/field_type_name.ts create mode 100644 src/plugins/discover/public/utils/get_field_type_name.test.ts rename src/plugins/discover/public/{application/main/components/sidebar/lib => utils}/get_field_type_name.ts (63%) diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index 40bc0a9f6df14..3fc5727c2838a 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -29,7 +29,7 @@ import type { DataViewField, DataView } from '@kbn/data-views-plugin/public'; import { getTypeForFieldIcon } from '../../../../utils/get_type_for_field_icon'; import { DiscoverFieldDetails } from './discover_field_details'; import { FieldDetails } from './types'; -import { getFieldTypeName } from './lib/get_field_type_name'; +import { getFieldTypeName } from '../../../../utils/get_field_type_name'; import { DiscoverFieldVisualize } from './discover_field_visualize'; function wrapOnDot(str?: string) { diff --git a/src/plugins/discover/public/components/field_name/field_name.tsx b/src/plugins/discover/public/components/field_name/field_name.tsx index d937e58ff5256..3de9bd129fa35 100644 --- a/src/plugins/discover/public/components/field_name/field_name.tsx +++ b/src/plugins/discover/public/components/field_name/field_name.tsx @@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n'; import { FieldIcon, FieldIconProps } from '@kbn/react-field'; import { getFieldSubtypeMulti } from '@kbn/data-views-plugin/public'; import type { DataViewField } from '@kbn/data-views-plugin/public'; -import { getFieldTypeName } from './field_type_name'; +import { getFieldTypeName } from '../../utils/get_field_type_name'; interface Props { fieldName: string; diff --git a/src/plugins/discover/public/components/field_name/field_type_name.ts b/src/plugins/discover/public/components/field_name/field_type_name.ts deleted file mode 100644 index 3d3e97aba624b..0000000000000 --- a/src/plugins/discover/public/components/field_name/field_type_name.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; - -export function getFieldTypeName(type?: string) { - switch (type) { - case 'boolean': - return i18n.translate('discover.fieldNameIcons.booleanAriaLabel', { - defaultMessage: 'Boolean field', - }); - case 'conflict': - return i18n.translate('discover.fieldNameIcons.conflictFieldAriaLabel', { - defaultMessage: 'Conflicting field', - }); - case 'date': - return i18n.translate('discover.fieldNameIcons.dateFieldAriaLabel', { - defaultMessage: 'Date field', - }); - case 'geo_point': - return i18n.translate('discover.fieldNameIcons.geoPointFieldAriaLabel', { - defaultMessage: 'Geo point field', - }); - case 'geo_shape': - return i18n.translate('discover.fieldNameIcons.geoShapeFieldAriaLabel', { - defaultMessage: 'Geo shape field', - }); - case 'ip': - return i18n.translate('discover.fieldNameIcons.ipAddressFieldAriaLabel', { - defaultMessage: 'IP address field', - }); - case 'murmur3': - return i18n.translate('discover.fieldNameIcons.murmur3FieldAriaLabel', { - defaultMessage: 'Murmur3 field', - }); - case 'number': - return i18n.translate('discover.fieldNameIcons.numberFieldAriaLabel', { - defaultMessage: 'Number field', - }); - case 'source': - // Note that this type is currently not provided, type for _source is undefined - return i18n.translate('discover.fieldNameIcons.sourceFieldAriaLabel', { - defaultMessage: 'Source field', - }); - case 'string': - return i18n.translate('discover.fieldNameIcons.stringFieldAriaLabel', { - defaultMessage: 'String field', - }); - case 'nested': - return i18n.translate('discover.fieldNameIcons.nestedFieldAriaLabel', { - defaultMessage: 'Nested field', - }); - default: - return i18n.translate('discover.fieldNameIcons.unknownFieldAriaLabel', { - defaultMessage: 'Unknown field', - }); - } -} diff --git a/src/plugins/discover/public/utils/get_field_type_name.test.ts b/src/plugins/discover/public/utils/get_field_type_name.test.ts new file mode 100644 index 0000000000000..b612522d1c3ea --- /dev/null +++ b/src/plugins/discover/public/utils/get_field_type_name.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + getFieldTypeName, + KNOWN_FIELD_TYPES, + UNKNOWN_FIELD_TYPE_MESSAGE, +} from './get_field_type_name'; + +describe('getFieldTypeName', () => { + describe('known field types should be recognized', () => { + it.each(Object.values(KNOWN_FIELD_TYPES))( + `'%s' should return a string that does not match '${UNKNOWN_FIELD_TYPE_MESSAGE}'`, + (field) => { + const fieldTypeName = getFieldTypeName(field); + expect(typeof fieldTypeName).toBe('string'); + expect(fieldTypeName).not.toBe(UNKNOWN_FIELD_TYPE_MESSAGE); + } + ); + }); + + it(`should return '${UNKNOWN_FIELD_TYPE_MESSAGE}' when passed undefined`, () => { + expect(getFieldTypeName(undefined)).toBe(UNKNOWN_FIELD_TYPE_MESSAGE); + }); + + it(`should return '${UNKNOWN_FIELD_TYPE_MESSAGE}' when passed an unknown field type`, () => { + expect(getFieldTypeName('unknown_field_type')).toBe(UNKNOWN_FIELD_TYPE_MESSAGE); + }); +}); diff --git a/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_name.ts b/src/plugins/discover/public/utils/get_field_type_name.ts similarity index 63% rename from src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_name.ts rename to src/plugins/discover/public/utils/get_field_type_name.ts index 731f860058737..24d5e4602f3be 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_name.ts +++ b/src/plugins/discover/public/utils/get_field_type_name.ts @@ -7,70 +7,92 @@ */ import { i18n } from '@kbn/i18n'; +import { KBN_FIELD_TYPES, ES_FIELD_TYPES } from '@kbn/data-plugin/public'; -export function getFieldTypeName(type: string) { +export const KNOWN_FIELD_TYPES = { + BOOLEAN: KBN_FIELD_TYPES.BOOLEAN, + CONFLICT: KBN_FIELD_TYPES.CONFLICT, + DATE: KBN_FIELD_TYPES.DATE, + GEO_POINT: KBN_FIELD_TYPES.GEO_POINT, + GEO_SHAPE: KBN_FIELD_TYPES.GEO_SHAPE, + IP: KBN_FIELD_TYPES.IP, + KEYWORD: ES_FIELD_TYPES.KEYWORD, + MURMUR3: KBN_FIELD_TYPES.MURMUR3, + NUMBER: KBN_FIELD_TYPES.NUMBER, + NESTED: KBN_FIELD_TYPES.NESTED, + SOURCE: 'source', + STRING: KBN_FIELD_TYPES.STRING, + TEXT: ES_FIELD_TYPES.TEXT, + VERSION: ES_FIELD_TYPES.VERSION, +}; + +export const UNKNOWN_FIELD_TYPE_MESSAGE = i18n.translate( + 'discover.fieldNameIcons.unknownFieldAriaLabel', + { + defaultMessage: 'Unknown field', + } +); + +export function getFieldTypeName(type?: string) { switch (type) { - case 'boolean': + case KNOWN_FIELD_TYPES.BOOLEAN: return i18n.translate('discover.fieldNameIcons.booleanAriaLabel', { defaultMessage: 'Boolean field', }); - case 'conflict': + case KNOWN_FIELD_TYPES.CONFLICT: return i18n.translate('discover.fieldNameIcons.conflictFieldAriaLabel', { defaultMessage: 'Conflicting field', }); - case 'date': + case KNOWN_FIELD_TYPES.DATE: return i18n.translate('discover.fieldNameIcons.dateFieldAriaLabel', { defaultMessage: 'Date field', }); - case 'geo_point': + case KNOWN_FIELD_TYPES.GEO_POINT: return i18n.translate('discover.fieldNameIcons.geoPointFieldAriaLabel', { defaultMessage: 'Geo point field', }); - case 'geo_shape': + case KNOWN_FIELD_TYPES.GEO_SHAPE: return i18n.translate('discover.fieldNameIcons.geoShapeFieldAriaLabel', { defaultMessage: 'Geo shape field', }); - case 'ip': + case KNOWN_FIELD_TYPES.IP: return i18n.translate('discover.fieldNameIcons.ipAddressFieldAriaLabel', { defaultMessage: 'IP address field', }); - case 'murmur3': + case KNOWN_FIELD_TYPES.MURMUR3: return i18n.translate('discover.fieldNameIcons.murmur3FieldAriaLabel', { defaultMessage: 'Murmur3 field', }); - case 'number': + case KNOWN_FIELD_TYPES.NUMBER: return i18n.translate('discover.fieldNameIcons.numberFieldAriaLabel', { defaultMessage: 'Number field', }); - case 'source': + case KNOWN_FIELD_TYPES.SOURCE: // Note that this type is currently not provided, type for _source is undefined return i18n.translate('discover.fieldNameIcons.sourceFieldAriaLabel', { defaultMessage: 'Source field', }); - case 'string': + case KNOWN_FIELD_TYPES.STRING: return i18n.translate('discover.fieldNameIcons.stringFieldAriaLabel', { defaultMessage: 'String field', }); - case 'text': + case KNOWN_FIELD_TYPES.TEXT: return i18n.translate('discover.fieldNameIcons.textFieldAriaLabel', { defaultMessage: 'Text field', }); - case 'keyword': + case KNOWN_FIELD_TYPES.KEYWORD: return i18n.translate('discover.fieldNameIcons.keywordFieldAriaLabel', { defaultMessage: 'Keyword field', }); - - case 'nested': + case KNOWN_FIELD_TYPES.NESTED: return i18n.translate('discover.fieldNameIcons.nestedFieldAriaLabel', { defaultMessage: 'Nested field', }); - case 'version': + case KNOWN_FIELD_TYPES.VERSION: return i18n.translate('discover.fieldNameIcons.versionFieldAriaLabel', { defaultMessage: 'Version field', }); default: - return i18n.translate('discover.fieldNameIcons.unknownFieldAriaLabel', { - defaultMessage: 'Unknown field', - }); + return UNKNOWN_FIELD_TYPE_MESSAGE; } } From f1769edb19b3972caa535d5ec12b53c0ee99e855 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 9 Jun 2022 17:49:28 -0700 Subject: [PATCH 09/62] [DOCS] Add prerequisites in create rule API (#133978) --- docs/api/alerting.asciidoc | 2 +- docs/api/alerting/create_rule.asciidoc | 182 ++++++++++++++----------- 2 files changed, 106 insertions(+), 78 deletions(-) diff --git a/docs/api/alerting.asciidoc b/docs/api/alerting.asciidoc index 931165ce5f485..44ed7b3b88739 100644 --- a/docs/api/alerting.asciidoc +++ b/docs/api/alerting.asciidoc @@ -31,7 +31,7 @@ The following APIs are available for Alerting. For deprecated APIs, refer to <>. -include::alerting/create_rule.asciidoc[] +include::alerting/create_rule.asciidoc[leveloffset=+1] include::alerting/update_rule.asciidoc[] include::alerting/get_rules.asciidoc[] include::alerting/delete_rule.asciidoc[] diff --git a/docs/api/alerting/create_rule.asciidoc b/docs/api/alerting/create_rule.asciidoc index 79ae7b0c39d6c..484866436d97d 100644 --- a/docs/api/alerting/create_rule.asciidoc +++ b/docs/api/alerting/create_rule.asciidoc @@ -1,5 +1,5 @@ [[create-rule-api]] -=== Create rule API +== Create rule API ++++ Create rule ++++ @@ -7,13 +7,23 @@ Create {kib} rules. [[create-rule-api-request]] -==== Request +=== {api-request-title} `POST :/api/alerting/rule/` `POST :/s//api/alerting/rule/` -==== {api-description-title} + +=== {api-prereq-title} + +You must have `all` privileges for the *Management* > *Stack Rules* feature or +for the *{ml-app}*, *{observability}*, or *Security* features, depending on the +`consumer` and `rule_type_id` of the rule you're creating. If the rule has +`actions`, you must also have `read` privileges for the *Management* > +*Actions and Connectors* feature. For more details, refer to +<>. + +=== {api-description-title} [WARNING] ==== @@ -25,84 +35,109 @@ If a user with different privileges updates the rule, its behavior might change. ==== [[create-rule-api-path-params]] -==== Path parameters +=== {api-path-parms-title} ``:: - (Optional, string) Specifies a UUID v1 or v4 to use instead of a randomly generated ID. +(Optional, string) Specifies a UUID v1 or v4 to use instead of a randomly +generated ID. `space_id`:: - (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. +(Optional, string) An identifier for the space. If `space_id` is not provided in +the URL, the default space is used. +[role="child_attributes"] [[create-rule-api-request-body]] -==== Request body - -`name`:: - (Required, string) A name to reference and search. - -`tags`:: - (Optional, string array) A list of keywords to reference and search. - -`rule_type_id`:: - (Required, string) The ID of the rule type that you want to call when the rule is scheduled to run. +=== {api-request-body-title} -`schedule`:: - (Required, object) The schedule specifying when this rule should be run, using one of the available schedule formats specified under +`actions`:: +(Optional, object array) An array of action objects. + -._Schedule Formats_. +.Properties of the action objects: [%collapsible%open] ===== -A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule. -We currently support the _Interval format_ which specifies the interval in seconds, minutes, hours or days at which the rule should execute. -Example: `{ interval: "10s" }`, `{ interval: "5m" }`, `{ interval: "1h" }`, `{ interval: "1d" }`. +`group`::: +(Required, string) Grouping actions is recommended for escalations for different +types of alerts. If you don't need this, set this value to `default`. -There are plans to support multiple other schedule formats in the near future. -===== +`id`::: +(Required, string) The ID of the connector saved object. -`throttle`:: - (Optional, string) How often this rule should fire the same actions. This will prevent the rule from sending out the same notification over and over. For example, if a rule with a `schedule` of 1 minute stays in a triggered state for 90 minutes, setting a `throttle` of `10m` or `1h` will prevent it from sending 90 notifications during this period. +`params`::: +(Required, object) The map to the `params` that the +<> will receive. ` params` are handled as Mustache +templates and passed a default set of context. +===== -`notify_when`:: - (Required, string) The condition for throttling the notification: `onActionGroupChange`, `onActiveAlert`, or `onThrottleInterval`. +`consumer`:: +(Required, string) The name of the application or feature that owns the rule. +For example: `alerts`, `apm`, `discover`, `infrastructure`, `logs`, `metrics`, +`ml`, `monitoring`, `securitySolution`, `siem`, `stackAlerts`, or `uptime`. `enabled`:: - (Optional, boolean) Indicates if you want to run the rule on an interval basis after it is created. +(Optional, boolean) Indicates if you want to run the rule on an interval basis +after it is created. -`consumer`:: - (Required, string) The name of the application that owns the rule. This name has to match the Kibana Feature name, as that dictates the required RBAC privileges. +`name`:: +(Required, string) A name to reference and search. + +`notify_when`:: +(Required, string) The condition for throttling the notification: +`onActionGroupChange`, `onActiveAlert`, or `onThrottleInterval`. `params`:: - (Required, object) The parameters to pass to the rule type executor `params` value. This will also validate against the rule type params validator, if defined. +(Required, object) The parameters to pass to the rule type executor `params` +value. This will also validate against the rule type params validator, if defined. -`actions`:: - (Optional, object array) An array of the following action objects. +`rule_type_id`:: +(Required, string) The ID of the rule type that you want to call when the rule +is scheduled to run. For example, `.es-query`, `.index-threshold`, +`logs.alert.document.count`, `monitoring_alert_cluster_health`, +`siem.thresholdRule`, or `xpack.ml.anomaly_detection_alert`. For more +information, refer to <>. + +`schedule`:: +(Required, object) The schedule specifying when this rule should be run, using +one of the available schedule formats. + -.Properties of the action objects: +.Schedule formats [%collapsible%open] ===== - `group`::: - (Required, string) Grouping actions is recommended for escalations for different types of alerts. If you don't need this, set this value to `default`. +A schedule is structured such that the key specifies the format you wish to use +and its value specifies the schedule. - `id`::: - (Required, string) The ID of the connector saved object to execute. +We currently support the _interval format_ which specifies the interval in +seconds, minutes, hours or days at which the rule should run. For example: +`{ "interval": "10s" }`, `{ "interval": "5m" }`, `{ "interval": "1h" }`, or +`{ "interval": "1d" }`. - `params`::: - (Required, object) The map to the `params` that the <> will receive. ` params` are handled as Mustache templates and passed a default set of context. +There are plans to support multiple other schedule formats in the near future. ===== +`tags`:: +(Optional, string array) A list of keywords to reference and search. + +`throttle`:: +(Optional, string) How often this rule should fire the same actions. This will +prevent the rule from sending out the same notification over and over. For +example, if a rule with a `schedule` of 1 minute stays in a triggered state for +90 minutes, setting a `throttle` of `10m` or `1h` will prevent it from sending +90 notifications during this period. [[create-rule-api-request-codes]] -==== Response code +=== {api-response-codes-title} `200`:: Indicates a successful call. [[create-rule-api-example]] -==== Example +=== {api-examples-title} + +Create a rule that has actions associated with a server log connector: [source,sh] -------------------------------------------------- -$ curl -X POST api/alerting/rule -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d ' +POST api/alerting/rule { "params":{ "aggType":"avg", @@ -141,7 +176,7 @@ $ curl -X POST api/alerting/rule -H 'kbn-xsrf: true' -H 'Content-Type: applicat ], "notify_when":"onActionGroupChange", "name":"my alert" -}' +} -------------------------------------------------- // KIBANA @@ -151,7 +186,12 @@ The API returns the following: -------------------------------------------------- { "id": "41893910-6bca-11eb-9e0d-85d233e3ee35", - "notify_when": "onActionGroupChange", + "consumer": "alerts", + "tags": ["cpu"], + "name": "my alert", + "enabled": true, + "throttle": null, + "schedule": {"interval": "1m"}, "params": { "aggType": "avg", "termSize": 6, @@ -159,49 +199,37 @@ The API returns the following: "timeWindowSize": 5, "timeWindowUnit": "m", "groupBy": "top", - "threshold": [ - 1000 - ], - "index": [ - ".kibana" - ], + "threshold": [1000], + "index": [".test-index"], "timeField": "@timestamp", "aggField": "sheet.version", "termField": "name.keyword" }, - "consumer": "alerts", "rule_type_id": ".index-threshold", - "schedule": { - "interval": "1m" - }, + "scheduled_task_id": "425b0800-6bca-11eb-9e0d-85d233e3ee35", + "snooze_schedule":[], + "created_by": "elastic", + "updated_by": "elastic", + "created_at": "2022-06-08T17:20:31.632Z", + "updated_at": "2022-06-08T17:20:31.632Z", + "api_key_owner": "elastic", + "notify_when": "onActionGroupChange", + "mute_all": false, + "muted_alert_ids": [], + "execution_status": { + "last_execution_date": "2022-06-08T17:20:31.632Z", + "status": "pending" + } "actions": [ { - "connector_type_id": ".server-log", "group": "threshold met", + "id": "dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2", "params": { "level": "info", "message": "alert {{alertName}} is active for group {{context.group}}:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}}" }, - "id": "dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2" + "connector_type_id": ".server-log" } - ], - "tags": [ - "cpu" - ], - "name": "my alert", - "enabled": true, - "throttle": null, - "api_key_owner": "elastic", - "created_by": "elastic", - "updated_by": "elastic", - "mute_all": false, - "muted_alert_ids": [], - "updated_at": "2021-02-10T18:03:19.961Z", - "created_at": "2021-02-10T18:03:19.961Z", - "scheduled_task_id": "425b0800-6bca-11eb-9e0d-85d233e3ee35", - "execution_status": { - "last_execution_date": "2021-02-10T18:03:19.966Z", - "status": "pending" - } + ] } -------------------------------------------------- From 6f2a7f487cea6c2ff5532722ac64a811c840c60e Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Fri, 10 Jun 2022 07:06:50 +0200 Subject: [PATCH 10/62] [Discover] Bypass no-data check in dev mode (#133461) * [Discover] Bypass no-data check if dev environment * [Discover] Bypass no-data check if dev environment * Fix typescript issues * Addressing PR comments * Fix type issue Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/application/discover_router.test.tsx | 9 ++++++--- .../public/application/discover_router.tsx | 6 +++--- src/plugins/discover/public/application/index.tsx | 8 ++++---- .../application/main/discover_main_route.tsx | 15 ++++++++++----- src/plugins/discover/public/plugin.tsx | 4 ++-- 5 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/plugins/discover/public/application/discover_router.test.tsx b/src/plugins/discover/public/application/discover_router.test.tsx index 9d8d66f333137..fd3dae2b7241d 100644 --- a/src/plugins/discover/public/application/discover_router.test.tsx +++ b/src/plugins/discover/public/application/discover_router.test.tsx @@ -18,9 +18,12 @@ import { ContextAppRoute } from './context'; const pathMap: Record = {}; describe('Discover router', () => { + const props = { + isDev: false, + }; beforeAll(() => { const { history } = createSearchSessionMock(); - const component = shallow(discoverRouter(mockDiscoverServices, history)); + const component = shallow(discoverRouter(mockDiscoverServices, history, props.isDev)); component.find(Route).forEach((route) => { const routeProps = route.props() as RouteProps; const path = routeProps.path; @@ -33,11 +36,11 @@ describe('Discover router', () => { }); it('should show DiscoverMainRoute component for / route', () => { - expect(pathMap['/']).toMatchObject(); + expect(pathMap['/']).toMatchObject(); }); it('should show DiscoverMainRoute component for /view/:id route', () => { - expect(pathMap['/view/:id']).toMatchObject(); + expect(pathMap['/view/:id']).toMatchObject(); }); it('should show SingleDocRoute component for /doc/:indexPatternId/:index route', () => { diff --git a/src/plugins/discover/public/application/discover_router.tsx b/src/plugins/discover/public/application/discover_router.tsx index 6950d03927b8c..8a5d1fee7f3bb 100644 --- a/src/plugins/discover/public/application/discover_router.tsx +++ b/src/plugins/discover/public/application/discover_router.tsx @@ -18,7 +18,7 @@ import { NotFoundRoute } from './not_found'; import { DiscoverServices } from '../build_services'; import { ViewAlertRoute } from './view_alert'; -export const discoverRouter = (services: DiscoverServices, history: History) => ( +export const discoverRouter = (services: DiscoverServices, history: History, isDev: boolean) => ( @@ -41,10 +41,10 @@ export const discoverRouter = (services: DiscoverServices, history: History) => - + - + diff --git a/src/plugins/discover/public/application/index.tsx b/src/plugins/discover/public/application/index.tsx index 8747492640065..5ae2ed76923b5 100644 --- a/src/plugins/discover/public/application/index.tsx +++ b/src/plugins/discover/public/application/index.tsx @@ -10,7 +10,7 @@ import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public'; import { discoverRouter } from './discover_router'; import { DiscoverServices } from '../build_services'; -export const renderApp = (element: HTMLElement, services: DiscoverServices) => { +export const renderApp = (element: HTMLElement, services: DiscoverServices, isDev: boolean) => { const { history: getHistory, capabilities, chrome, data, core } = services; const history = getHistory(); @@ -25,9 +25,9 @@ export const renderApp = (element: HTMLElement, services: DiscoverServices) => { iconType: 'glasses', }); } - const unmount = toMountPoint(wrapWithTheme(discoverRouter(services, history), core.theme.theme$))( - element - ); + const unmount = toMountPoint( + wrapWithTheme(discoverRouter(services, history, isDev), core.theme.theme$) + )(element); return () => { unmount(); diff --git a/src/plugins/discover/public/application/main/discover_main_route.tsx b/src/plugins/discover/public/application/main/discover_main_route.tsx index 78a34a318e646..006e43effc1bf 100644 --- a/src/plugins/discover/public/application/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.tsx @@ -39,9 +39,14 @@ interface DiscoverLandingParams { id: string; } -export function DiscoverMainRoute() { +interface Props { + isDev: boolean; +} + +export function DiscoverMainRoute(props: Props) { const history = useHistory(); const services = useDiscoverServices(); + const { isDev } = props; const { core, chrome, @@ -75,8 +80,8 @@ export function DiscoverMainRoute() { .hasUserDataView() .catch(() => false); - const hasESDataValue = await data.dataViews.hasData.hasESData().catch(() => false); - + const hasESDataValue = + isDev || (await data.dataViews.hasData.hasESData().catch(() => false)); setHasUserDataView(hasUserDataViewValue); setHasESData(hasESDataValue); @@ -106,7 +111,7 @@ export function DiscoverMainRoute() { setError(e); } }, - [config, data.dataViews, history, toastNotifications] + [config, data.dataViews, history, isDev, toastNotifications] ); const loadSavedSearch = useCallback(async () => { @@ -208,7 +213,7 @@ export function DiscoverMainRoute() { // We've already called this, so we can optimize the analytics services to // use the already-retrieved data to avoid a double-call. - hasESData: () => Promise.resolve(hasESData), + hasESData: () => Promise.resolve(isDev ? true : hasESData), hasUserDataView: () => Promise.resolve(hasUserDataView), }, }, diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index b4a33119c23b2..25bc46bf90c2b 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -191,7 +191,7 @@ export class DiscoverPlugin setup(core: CoreSetup, plugins: DiscoverSetupPlugins) { const baseUrl = core.http.basePath.prepend('/app/discover'); - + const isDev = this.initializerContext.env.mode.dev; if (plugins.share) { this.locator = plugins.share.url.locators.create( new DiscoverAppLocatorDefinition({ @@ -291,7 +291,7 @@ export class DiscoverPlugin // FIXME: Temporarily hide overflow-y in Discover app when Field Stats table is shown // due to EUI bug https://github.com/elastic/eui/pull/5152 params.element.classList.add('dscAppWrapper'); - const unmount = renderApp(params.element, services); + const unmount = renderApp(params.element, services, isDev); return () => { unlistenParentHistory(); unmount(); From cb71390ed52544c84270d0aaf99089301b731e49 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Fri, 10 Jun 2022 11:26:58 +0300 Subject: [PATCH 11/62] =?UTF-8?q?FIx=20[test-failed]:=20Chrome=20X-Pack=20?= =?UTF-8?q?UI=20Functional=20Tests.x-pack/test/functional/apps/lens/group1?= =?UTF-8?q?/persistent=5Fcontext=C2=B7ts=20-=20lens=20app=20-=20group=201?= =?UTF-8?q?=20lens=20query=20context=20keeps=20selected=20index=20pattern?= =?UTF-8?q?=20after=20refresh=20(#134039)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/functional/page_objects/unified_search_page.ts | 13 ++++++++++++- x-pack/test/functional/page_objects/lens_page.ts | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/test/functional/page_objects/unified_search_page.ts b/test/functional/page_objects/unified_search_page.ts index ad964383641d4..af3cbd05c53e4 100644 --- a/test/functional/page_objects/unified_search_page.ts +++ b/test/functional/page_objects/unified_search_page.ts @@ -34,7 +34,18 @@ export class UnifiedSearchPageObject extends FtrService { await this.retry.waitFor( 'wait for updating switcher', - async () => (await this.testSubjects.getVisibleText(switchButtonSelector)) === dataViewTitle + async () => (await this.getSelectedDataView(switchButtonSelector)) === dataViewTitle ); } + + public async getSelectedDataView(switchButtonSelector: string) { + let visibleText = ''; + + await this.retry.waitFor('wait for updating switcher', async () => { + visibleText = await this.testSubjects.getVisibleText(switchButtonSelector); + return Boolean(visibleText); + }); + + return visibleText; + } } diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index c21efa55f167d..d1125fbb08175 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -860,14 +860,14 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont * Returns the current index pattern of the data panel */ async getDataPanelIndexPattern() { - return await (await testSubjects.find('lns-dataView-switch-link')).getAttribute('title'); + return await PageObjects.unifiedSearch.getSelectedDataView('lns-dataView-switch-link'); }, /** * Returns the current index pattern of the first layer */ async getFirstLayerIndexPattern() { - return await (await testSubjects.find('lns_layerIndexPatternLabel')).getAttribute('title'); + return await PageObjects.unifiedSearch.getSelectedDataView('lns_layerIndexPatternLabel'); }, async linkedToOriginatingApp() { From d52b866c86dd85f27d074e81ad830db730481c41 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Fri, 10 Jun 2022 12:27:14 +0300 Subject: [PATCH 12/62] [Canvas] Markdown element auto-applies text changes. (#133318) * Added editor argument with auto applying of the text. * Added comment. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../uis/arguments/editor.tsx | 86 +++++++++++++++++++ .../canvas_plugin_src/uis/arguments/index.ts | 2 + .../canvas_plugin_src/uis/views/markdown.js | 8 +- x-pack/plugins/canvas/i18n/ui.ts | 10 +++ .../with_debounce_arg/with_debounce_arg.tsx | 7 ++ 5 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/editor.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/editor.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/editor.tsx new file mode 100644 index 0000000000000..c96024658048c --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/editor.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useEffect, useCallback, FC } from 'react'; +import { EuiFormRow } from '@elastic/eui'; +import { LangModuleType } from '@kbn/monaco'; +import { CodeEditorField } from '@kbn/kibana-react-plugin/public'; +import usePrevious from 'react-use/lib/usePrevious'; +import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; +import { ArgumentStrings } from '../../../i18n'; +import { withDebounceArg } from '../../../public/components/with_debounce_arg'; + +const { Editor: strings } = ArgumentStrings; + +interface EditorArgProps { + onValueChange: (value: string) => void; + argValue: string; + typeInstance?: { + options: { + language: LangModuleType['ID']; + }; + }; + renderError: (err?: string | Error) => void; +} + +const EditorArg: FC = ({ argValue, typeInstance, onValueChange, renderError }) => { + const [value, setValue] = useState(argValue); + const prevValue = usePrevious(value); + + const onChange = useCallback((text: string) => setValue(text), [setValue]); + + useEffect(() => { + onValueChange(value); + }, [onValueChange, value]); + + useEffect(() => { + // update editor content, if it has been changed from within the expression. + if (prevValue === value && argValue !== value) { + setValue(argValue); + } + }, [argValue, setValue, prevValue, value]); + + if (typeof argValue !== 'string') { + renderError(); + return null; + } + + const { language } = typeInstance?.options ?? {}; + + return ( + + { + const model = editor.getModel(); + model?.updateOptions({ tabSize: 2 }); + }} + /> + + ); +}; + +export const editor = () => ({ + name: 'editor', + displayName: strings.getDisplayName(), + help: strings.getHelp(), + template: templateFromReactComponent(withDebounceArg(EditorArg, 250)), +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts index 40e98d18a706e..897422edaeb76 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts @@ -33,6 +33,7 @@ import { textarea } from './textarea'; import { toggle } from './toggle'; import { visdimension } from './vis_dimension'; import { colorPicker } from './color_picker'; +import { editor } from './editor'; import { SetupInitializer } from '../../plugin'; @@ -53,6 +54,7 @@ export const args = [ toggle, visdimension, colorPicker, + editor, ]; export const initializers = [dateFormatInitializer, numberFormatInitializer]; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/markdown.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/markdown.js index d6ebac0e304e6..72e4ec9bc152d 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/markdown.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/markdown.js @@ -5,6 +5,7 @@ * 2.0. */ +import { MarkdownLang } from '@kbn/kibana-react-plugin/public'; import { ViewStrings } from '../../../i18n'; const { Markdown: strings } = ViewStrings; @@ -20,11 +21,8 @@ export const markdown = () => ({ name: '_', displayName: strings.getContentDisplayName(), help: strings.getContentHelp(), - argType: 'textarea', - default: '""', - options: { - confirm: 'Apply', - }, + argType: 'editor', + options: { language: MarkdownLang }, multi: true, }, { diff --git a/x-pack/plugins/canvas/i18n/ui.ts b/x-pack/plugins/canvas/i18n/ui.ts index 2448db2d99904..a449598038098 100644 --- a/x-pack/plugins/canvas/i18n/ui.ts +++ b/x-pack/plugins/canvas/i18n/ui.ts @@ -348,6 +348,16 @@ export const ArgumentStrings = { defaultMessage: 'Provides colors for the values, based on the bounds', }), }, + Editor: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.arguments.editorTitle', { + defaultMessage: 'Editor', + }), + getHelp: () => + i18n.translate('xpack.canvas.uis.arguments.editorLabel', { + defaultMessage: 'Provides a text area with syntax highlighting', + }), + }, }; export const DataSourceStrings = { diff --git a/x-pack/plugins/canvas/public/components/with_debounce_arg/with_debounce_arg.tsx b/x-pack/plugins/canvas/public/components/with_debounce_arg/with_debounce_arg.tsx index 61e953a4a432b..0f9cfbce006b3 100644 --- a/x-pack/plugins/canvas/public/components/with_debounce_arg/with_debounce_arg.tsx +++ b/x-pack/plugins/canvas/public/components/with_debounce_arg/with_debounce_arg.tsx @@ -29,6 +29,13 @@ export const withDebounceArg = [localArgValue] ); + // handle the changed argument from within the expression. + useEffect(() => { + if (argValue) { + setArgValue(argValue); + } + }, [argValue]); + useEffect(() => { return () => { cancel(); From be1ab85f56708fdd7166989f9729748a4a946dc5 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Fri, 10 Jun 2022 14:09:56 +0300 Subject: [PATCH 13/62] [Canvas] Fixes pointseries don't get updated on datasource change (#132831) * Prevented Display tab from unmounting. Added fix of math fn arg bug with the empty column. * Added the possibility to pass the empty column. * Added comments. * Fixed bug with tons of re-renderings. * Fixed problems with frequent updating of fns. * Added comments. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../uis/arguments/datacolumn/index.js | 45 ++++++-- .../public/components/function_form/index.tsx | 12 +- .../element_settings.component.tsx | 106 +++++++++++------- .../canvas/public/state/actions/elements.js | 101 ++++++++++------- 4 files changed, 167 insertions(+), 97 deletions(-) diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/index.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/index.js index 987fd11c5faef..14ae4db1cb127 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/index.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/index.js @@ -10,6 +10,8 @@ import PropTypes from 'prop-types'; import { EuiSelect, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { sortBy } from 'lodash'; import { getType } from '@kbn/interpreter'; +import usePrevious from 'react-use/lib/usePrevious'; +import deepEqual from 'react-fast-compare'; import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component'; import { ArgumentStrings } from '../../../../i18n'; import { SimpleMathFunction } from './simple_math_function'; @@ -29,8 +31,8 @@ const getMathValue = (argValue, columns) => { const val = matchedCol ? maybeQuoteValue(matchedCol.name) : argValue; const mathValue = getFormObject(val); - const validColumn = columns.some(({ name }) => mathValue.column === name); - return { ...mathValue, column: validColumn ? mathValue.column : '' }; + const isValidColumn = columns.some(({ name }) => mathValue.column === name); + return { ...mathValue, column: mathValue.column, isValidColumn }; } catch (e) { return { error: e.message }; } @@ -46,11 +48,7 @@ const DatacolumnArgInput = ({ typeInstance, }) => { const [mathValue, setMathValue] = useState(getMathValue(argValue, columns)); - - useEffect(() => { - setMathValue(getMathValue(argValue, columns)); - }, [argValue, columns]); - + const prevMathValue = usePrevious(mathValue); const allowedTypes = typeInstance.options.allowedTypes || false; const onlyShowMathFunctions = typeInstance.options.onlyMath || false; @@ -73,16 +71,41 @@ const DatacolumnArgInput = ({ if (valueNotSet(fn)) { return onValueChange(column); } - // fn has a value, so use it as a math.js expression onValueChange(`${fn}(${maybeQuoteValue(column)})`); }, - [mathValue, onValueChange, columns] + [onValueChange, columns, mathValue] ); + useEffect(() => { + const newMathValue = getMathValue(argValue, columns); + setMathValue(newMathValue); + }, [argValue, columns]); + + useEffect(() => { + if ( + !mathValue.error && + mathValue.column !== '' && + !mathValue.isValidColumn && + !deepEqual(mathValue, prevMathValue) + ) { + updateFunctionValue(mathValue.fn, columns[0].name); + } + }, [ + mathValue.fn, + mathValue.column, + columns, + updateFunctionValue, + mathValue.error, + mathValue.isValidColumn, + prevMathValue, + mathValue, + ]); + const onChangeFn = useCallback( - ({ target: { value } }) => updateFunctionValue(value, mathValue.column), - [mathValue.column, updateFunctionValue] + ({ target: { value } }) => + updateFunctionValue(value, mathValue.isValidColumn ? mathValue.column : ''), + [mathValue.column, mathValue.isValidColumn, updateFunctionValue] ); const onChangeColumn = useCallback( diff --git a/x-pack/plugins/canvas/public/components/function_form/index.tsx b/x-pack/plugins/canvas/public/components/function_form/index.tsx index c40b7d2a90ae7..cc6f9541eaa47 100644 --- a/x-pack/plugins/canvas/public/components/function_form/index.tsx +++ b/x-pack/plugins/canvas/public/components/function_form/index.tsx @@ -77,9 +77,11 @@ export const FunctionForm: React.FunctionComponent = (props) const addArgument = useCallback( (argName: string, argValue: string | Ast | null) => () => { - dispatch(addArgumentValue({ element, pageId, argName, value: argValue, path })); + dispatch( + addArgumentValue({ elementId: element?.id, pageId, argName, value: argValue, path }) + ); }, - [dispatch, element, pageId, path] + [dispatch, element?.id, pageId, path] ); const updateContext = useCallback(() => { @@ -88,9 +90,11 @@ export const FunctionForm: React.FunctionComponent = (props) const setArgument = useCallback( (argName: string, valueIndex: number) => (value: string | Ast | null) => { - dispatch(setArgumentValue({ element, pageId, argName, value, valueIndex, path })); + dispatch( + setArgumentValue({ elementId: element?.id, pageId, argName, value, valueIndex, path }) + ); }, - [dispatch, element, pageId, path] + [dispatch, element?.id, pageId, path] ); const deleteArgument = useCallback( diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx index 0dcafd012ee7c..6f1fabf26fecc 100644 --- a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx +++ b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import React, { FunctionComponent, useState } from 'react'; +import React, { FunctionComponent, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; -import { EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui'; +import { EuiTab, EuiTabs } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; // @ts-expect-error unconverted component @@ -46,53 +46,79 @@ interface Props { } export const ElementSettings: FunctionComponent = ({ element }) => { - const filtersTab = isExpressionWithFilters(element.expression) && { - id: 'filters', - name: strings.getFiltersTabLabel(), - content: ( -
    - -
    - ), - }; - - const tabs = [ - { - id: 'edit', - name: strings.getDisplayTabLabel(), - content: ( -
    -
    - -
    -
    - ), - }, - { - id: 'data', - name: strings.getDataTabLabel(), + const tabs = useMemo(() => { + const filtersTab = isExpressionWithFilters(element.expression) && { + id: 'filters', + name: strings.getFiltersTabLabel(), content: (
    - +
    ), - }, - ...(filtersTab ? [filtersTab] : []), - ]; + }; - const [selectedTab, setSelectedTab] = useState(tabs[0]); + return [ + { + id: 'edit', + name: strings.getDisplayTabLabel(), + content: ( +
    +
    + +
    +
    + ), + }, + { + id: 'data', + name: strings.getDataTabLabel(), + content: ( +
    + +
    + ), + }, + ...(filtersTab ? [filtersTab] : []), + ]; + }, [element]); - const onTabClick = (tab: EuiTabbedContentTab) => setSelectedTab(tab); + const [selectedTab, setSelectedTab] = useState(tabs[0].id); + + const onSelectedTabChanged = (id: string) => { + setSelectedTab(id); + }; - const getTab = (tabId: string) => tabs.filter((tab) => tab.id === tabId)[0] ?? tabs[0]; + const tabsHeaders = tabs.map((tab) => ( + onSelectedTabChanged(tab.id)} + isSelected={tab.id === selectedTab} + > + {tab.name} + + )); + + const tabsContent = useMemo( + () => + tabs.map(({ id, content }) => + id === selectedTab ? ( + content + ) : ( + // tabs must be hidden, but mounted, because `Display` tab, for example, + // contains args, which should react on input changes and change the expression, + // according to the logic they encapsulate. + // Good example: columns have changed, the args of expression `math` should be changed, containing the new columns. +
    {content}
    + ) + ), + [selectedTab, tabs] + ); return ( - + <> + {tabsHeaders} + {tabsContent} + ); }; diff --git a/x-pack/plugins/canvas/public/state/actions/elements.js b/x-pack/plugins/canvas/public/state/actions/elements.js index 72186abd38c94..eec01d881b9ac 100644 --- a/x-pack/plugins/canvas/public/state/actions/elements.js +++ b/x-pack/plugins/canvas/public/state/actions/elements.js @@ -7,7 +7,7 @@ import { createAction } from 'redux-actions'; import immutable from 'object-path-immutable'; -import { get, pick, cloneDeep, without, last } from 'lodash'; +import { get, pick, cloneDeep, without, last, debounce } from 'lodash'; import { toExpression, safeElementFromExpression } from '@kbn/interpreter'; import { createThunk } from '../../lib/create_thunk'; import { @@ -16,6 +16,7 @@ import { getNodeById, getNodes, getSelectedPageIndex, + getElementById, } from '../selectors/workpad'; import { getValue as getResolvedArgsValue } from '../selectors/resolved_args'; import { getDefaultElement } from '../defaults'; @@ -68,50 +69,55 @@ export const setMultiplePositions = createAction('setMultiplePosition', (reposit export const flushContext = createAction('flushContext'); export const flushContextAfterIndex = createAction('flushContextAfterIndex'); -export const fetchContext = createThunk( - 'fetchContext', - ({ dispatch, getState }, index, element, fullRefresh = false, path) => { - const pathToTarget = [...path.split('.'), 'chain']; - const chain = get(element, pathToTarget); - const invalidIndex = chain ? index >= chain.length : true; +const fetchContextFn = ({ dispatch, getState }, index, element, fullRefresh = false, path) => { + const pathToTarget = [...path.split('.'), 'chain']; + const chain = get(element, pathToTarget); + const invalidIndex = chain ? index >= chain.length : true; - if (!element || !chain || invalidIndex) { - throw new Error(strings.getInvalidArgIndexErrorMessage(index)); - } + if (!element || !chain || invalidIndex) { + throw new Error(strings.getInvalidArgIndexErrorMessage(index)); + } - // cache context as the previous index - const contextIndex = index - 1; - const contextPath = [element.id, 'expressionContext', path, contextIndex]; + // cache context as the previous index + const contextIndex = index - 1; + const contextPath = [element.id, 'expressionContext', path, contextIndex]; - // set context state to loading - dispatch(args.setLoading({ path: contextPath })); + // set context state to loading + dispatch(args.setLoading({ path: contextPath })); - // function to walk back up to find the closest context available - const getContext = () => getSiblingContext(getState(), element.id, contextIndex - 1, [path]); - const { index: prevContextIndex, context: prevContextValue } = - fullRefresh !== true ? getContext() : {}; + // function to walk back up to find the closest context available + const getContext = () => getSiblingContext(getState(), element.id, contextIndex - 1, [path]); + const { index: prevContextIndex, context: prevContextValue } = + fullRefresh !== true ? getContext() : {}; - // modify the ast chain passed to the interpreter - const astChain = chain.filter((exp, i) => { - if (prevContextValue != null) { - return i > prevContextIndex && i < index; - } - return i < index; - }); + // modify the ast chain passed to the interpreter + const astChain = chain.filter((exp, i) => { + if (prevContextValue != null) { + return i > prevContextIndex && i < index; + } + return i < index; + }); - const variables = getWorkpadVariablesAsObject(getState()); + const variables = getWorkpadVariablesAsObject(getState()); - const { expressions } = pluginServices.getServices(); - const elementWithNewAst = set(element, pathToTarget, astChain); + const { expressions } = pluginServices.getServices(); + const elementWithNewAst = set(element, pathToTarget, astChain); - // get context data from a partial AST - return expressions - .interpretAst(elementWithNewAst.ast, variables, prevContextValue) - .then((value) => { - dispatch(args.setValue({ path: contextPath, value })); - }); - } -); + // get context data from a partial AST + return expressions + .interpretAst(elementWithNewAst.ast, variables, prevContextValue) + .then((value) => { + dispatch(args.setValue({ path: contextPath, value })); + }); +}; + +// It is necessary to debounce fetching of the context in the situations +// when the components of the arguments update the expression. For example, suppose there are +// multiple datacolumns that change the column to the first one from the list after datasource update. +// In that case, it is necessary to fetch the context only for the last version of the expression. +const fetchContextFnDebounced = debounce(fetchContextFn, 100); + +export const fetchContext = createThunk('fetchContext', fetchContextFnDebounced); const fetchRenderableWithContextFn = ({ dispatch, getState }, element, ast, context) => { const argumentPath = [element.id, 'expressionRenderable']; @@ -129,6 +135,7 @@ const fetchRenderableWithContextFn = ({ dispatch, getState }, element, ast, cont const variables = getWorkpadVariablesAsObject(getState()); const { expressions, notify } = pluginServices.getServices(); + return expressions .runInterpreter(ast, context, variables, { castToRender: true }) .then((renderable) => { @@ -140,9 +147,15 @@ const fetchRenderableWithContextFn = ({ dispatch, getState }, element, ast, cont }); }; +// It is necessary to debounce fetching of the renderable with the context in the situations +// when the components of the arguments update the expression. For example, suppose there are +// multiple datacolumns that change the column to the first one from the list after datasource update. +// In that case, it is necessary to fetch the context only for the last version of the expression. +const fetchRenderableWithContextFnDebounced = debounce(fetchRenderableWithContextFn, 100); + export const fetchRenderableWithContext = createThunk( 'fetchRenderableWithContext', - fetchRenderableWithContextFn + fetchRenderableWithContextFnDebounced ); export const fetchRenderable = createThunk('fetchRenderable', ({ dispatch }, element) => { @@ -358,18 +371,20 @@ export const setAstAtIndex = createThunk( * @param {any} args.element - the element, which contains the expression. * @param {any} args.pageId - the workpad's page, where element is located. */ -export const setArgument = createThunk('setArgument', ({ dispatch }, args) => { - const { argName, value, valueIndex, element, pageId, path } = args; +export const setArgument = createThunk('setArgument', ({ dispatch, getState }, args) => { + const { argName, value, valueIndex, elementId, pageId, path } = args; let selector = `${path}.${argName}`; if (valueIndex != null) { selector += '.' + valueIndex; } + const element = getElementById(getState(), elementId); const newElement = set(element, selector, value); const pathTerms = path.split('.'); const argumentChainPath = pathTerms.slice(0, 3); const argumnentChainIndex = last(argumentChainPath); const newAst = get(newElement, argumentChainPath); + dispatch(setAstAtIndex(argumnentChainIndex, newAst, element, pageId)); }); @@ -381,13 +396,15 @@ export const setArgument = createThunk('setArgument', ({ dispatch }, args) => { * @param {any} args.element - the element, which contains the expression. * @param {any} args.pageId - the workpad's page, where element is located. */ -export const addArgumentValue = createThunk('addArgumentValue', ({ dispatch }, args) => { - const { argName, value, element, path } = args; +export const addArgumentValue = createThunk('addArgumentValue', ({ dispatch, getState }, args) => { + const { argName, value, elementId, path } = args; + const element = getElementById(getState(), elementId); const values = get(element, [...path.split('.'), argName], []); const newValue = values.concat(value); dispatch( setArgument({ ...args, + elementId, value: newValue, }) ); From 3afe9603022d53e7ddbeaa9ad33727a007f59a16 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Fri, 10 Jun 2022 08:12:39 -0500 Subject: [PATCH 14/62] [artifacts] fix download, partial revert of #134046 --- .buildkite/scripts/steps/artifacts/docker_context.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.buildkite/scripts/steps/artifacts/docker_context.sh b/.buildkite/scripts/steps/artifacts/docker_context.sh index d7bbe2ecb27cf..1195d7ad5dc38 100755 --- a/.buildkite/scripts/steps/artifacts/docker_context.sh +++ b/.buildkite/scripts/steps/artifacts/docker_context.sh @@ -26,5 +26,7 @@ fi tar -xf "target/$DOCKER_CONTEXT_FILE" -C "$DOCKER_BUILD_FOLDER" cd $DOCKER_BUILD_FOLDER +buildkite-agent artifact download "kibana-$FULL_VERSION-linux-x86_64.tar.gz" . --build "${KIBANA_BUILD_ID:-$BUILDKITE_BUILD_ID}" + echo "--- Build context" docker build . From 7604d3beef0feccc88a2eb28c48d4e0572fc7741 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 10 Jun 2022 19:14:47 +0200 Subject: [PATCH 15/62] [APM] Backend operations detail view + metric charts (#133866) * Metric charts for backend operation detail view * API tests * Review feedback Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../get_span_destination_metrics.ts | 2 +- .../03_span_destination_metrics.test.ts | 2 +- .../plugins/apm/common/time_range_metadata.ts | 10 + .../app/backend_detail_operations/index.tsx | 42 +-- .../app/backend_detail_overview/index.tsx | 54 +-- .../backend_operation_detail_view/index.tsx | 46 +++ .../public/components/routing/home/index.tsx | 28 +- .../routing/service_detail/index.tsx | 7 +- .../templates/backend_detail_template.tsx | 3 +- .../backend_error_rate_chart.tsx | 53 +-- .../backend_latency_chart.tsx | 57 ++-- .../backend_metric_charts_route_params.ts | 24 ++ .../backend_throughput_chart.tsx | 55 ++-- .../shared/backend_metric_charts/index.tsx | 91 ++++++ .../shared/detail_view_header/index.tsx | 45 +++ .../time_range_metadata_context.tsx | 64 ++++ .../use_search_service_destination_metrics.ts | 31 ++ .../use_time_range_metadata_context.ts | 30 ++ ...se_backend_detail_operations_breadcrumb.ts | 49 +++ ...et_is_using_service_destination_metrics.ts | 94 ++++++ .../get_global_apm_server_route_repository.ts | 27 +- .../get_error_rate_charts_for_backend.ts | 57 +++- .../get_latency_charts_for_backend.ts | 54 ++- .../get_throughput_charts_for_backend.ts | 36 +- .../apm/server/routes/backends/route.ts | 68 +++- .../routes/time_range_metadata/route.ts | 48 +++ .../dependencies/dependency_metrics.spec.ts | 308 ++++++++++++++++++ .../dependencies/generate_operation_data.ts | 84 +++++ .../tests/dependencies/top_operations.spec.ts | 91 ++---- .../throughput/dependencies_apis.spec.ts | 2 + 30 files changed, 1279 insertions(+), 283 deletions(-) create mode 100644 x-pack/plugins/apm/common/time_range_metadata.ts create mode 100644 x-pack/plugins/apm/public/components/app/backend_operation_detail_view/index.tsx rename x-pack/plugins/apm/public/components/{app/backend_detail_overview => shared/backend_metric_charts}/backend_error_rate_chart.tsx (72%) rename x-pack/plugins/apm/public/components/{app/backend_detail_overview => shared/backend_metric_charts}/backend_latency_chart.tsx (69%) create mode 100644 x-pack/plugins/apm/public/components/shared/backend_metric_charts/backend_metric_charts_route_params.ts rename x-pack/plugins/apm/public/components/{app/backend_detail_overview => shared/backend_metric_charts}/backend_throughput_chart.tsx (69%) create mode 100644 x-pack/plugins/apm/public/components/shared/backend_metric_charts/index.tsx create mode 100644 x-pack/plugins/apm/public/components/shared/detail_view_header/index.tsx create mode 100644 x-pack/plugins/apm/public/context/time_range_metadata/time_range_metadata_context.tsx create mode 100644 x-pack/plugins/apm/public/context/time_range_metadata/use_search_service_destination_metrics.ts create mode 100644 x-pack/plugins/apm/public/context/time_range_metadata/use_time_range_metadata_context.ts create mode 100644 x-pack/plugins/apm/public/hooks/use_backend_detail_operations_breadcrumb.ts create mode 100644 x-pack/plugins/apm/server/lib/helpers/spans/get_is_using_service_destination_metrics.ts create mode 100644 x-pack/plugins/apm/server/routes/time_range_metadata/route.ts create mode 100644 x-pack/test/apm_api_integration/tests/dependencies/dependency_metrics.spec.ts create mode 100644 x-pack/test/apm_api_integration/tests/dependencies/generate_operation_data.ts diff --git a/packages/elastic-apm-synthtrace/src/lib/apm/processors/get_span_destination_metrics.ts b/packages/elastic-apm-synthtrace/src/lib/apm/processors/get_span_destination_metrics.ts index b806948a0949b..4f04feb841dd4 100644 --- a/packages/elastic-apm-synthtrace/src/lib/apm/processors/get_span_destination_metrics.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/processors/get_span_destination_metrics.ts @@ -31,7 +31,7 @@ export function getSpanDestinationMetrics(events: ApmFields[]) { return { ...metricset.key, - ['metricset.name']: 'span_destination', + ['metricset.name']: 'service_destination', 'span.destination.service.response_time.sum.us': sum, 'span.destination.service.response_time.count': count, }; diff --git a/packages/elastic-apm-synthtrace/src/test/scenarios/03_span_destination_metrics.test.ts b/packages/elastic-apm-synthtrace/src/test/scenarios/03_span_destination_metrics.test.ts index 1e8630ebaa650..39c33e6486f69 100644 --- a/packages/elastic-apm-synthtrace/src/test/scenarios/03_span_destination_metrics.test.ts +++ b/packages/elastic-apm-synthtrace/src/test/scenarios/03_span_destination_metrics.test.ts @@ -68,7 +68,7 @@ describe('span destination metrics', () => { ) ) ) - .filter((fields) => fields['metricset.name'] === 'span_destination'); + .filter((fields) => fields['metricset.name'] === 'service_destination'); }); it('generates the right amount of span metrics', () => { diff --git a/x-pack/plugins/apm/common/time_range_metadata.ts b/x-pack/plugins/apm/common/time_range_metadata.ts new file mode 100644 index 0000000000000..61e18a71ae543 --- /dev/null +++ b/x-pack/plugins/apm/common/time_range_metadata.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface TimeRangeMetadata { + isUsingServiceDestinationMetrics: boolean; +} diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_operations/index.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_operations/index.tsx index f26f9d66833ee..7b4fd37973811 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_operations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_operations/index.tsx @@ -4,49 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { i18n } from '@kbn/i18n'; + import React from 'react'; -import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb'; -import { useApmParams } from '../../../hooks/use_apm_params'; -import { useApmRouter } from '../../../hooks/use_apm_router'; +import { useBackendDetailOperationsBreadcrumb } from '../../../hooks/use_backend_detail_operations_breadcrumb'; import { BackendDetailOperationsList } from './backend_detail_operations_list'; export function BackendDetailOperations() { - const { - query: { - backendName, - rangeFrom, - rangeTo, - refreshInterval, - refreshPaused, - environment, - kuery, - comparisonEnabled, - }, - } = useApmParams('/backends/operations'); - - const apmRouter = useApmRouter(); - - useBreadcrumb([ - { - title: i18n.translate( - 'xpack.apm.backendDetailOperations.breadcrumbTitle', - { defaultMessage: 'Operations' } - ), - href: apmRouter.link('/backends/operations', { - query: { - backendName, - rangeFrom, - rangeTo, - refreshInterval, - refreshPaused, - environment, - kuery, - comparisonEnabled, - }, - }), - }, - ]); + useBackendDetailOperationsBreadcrumb(); return ; } diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx index 76089c4c756ac..63b87f4efcbda 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx @@ -4,22 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EuiFlexItem } from '@elastic/eui'; -import { EuiPanel } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; import React from 'react'; import { EuiSpacer } from '@elastic/eui'; -import { EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb'; import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; import { useApmParams } from '../../../hooks/use_apm_params'; import { useApmRouter } from '../../../hooks/use_apm_router'; -import { BackendLatencyChart } from './backend_latency_chart'; import { BackendDetailDependenciesTable } from './backend_detail_dependencies_table'; -import { BackendThroughputChart } from './backend_throughput_chart'; -import { BackendFailedTransactionRateChart } from './backend_error_rate_chart'; -import { useBreakpoints } from '../../../hooks/use_breakpoints'; +import { BackendMetricCharts } from '../../shared/backend_metric_charts'; export function BackendDetailOverview() { const { @@ -56,54 +49,11 @@ export function BackendDetailOverview() { }), }, ]); - const largeScreenOrSmaller = useBreakpoints().isLarge; return ( <> - - - - -

    - {i18n.translate('xpack.apm.backendDetailLatencyChartTitle', { - defaultMessage: 'Latency', - })} -

    -
    - -
    -
    - - - -

    - {i18n.translate( - 'xpack.apm.backendDetailThroughputChartTitle', - { defaultMessage: 'Throughput' } - )} -

    -
    - -
    -
    - - - -

    - {i18n.translate( - 'xpack.apm.backendDetailFailedTransactionRateChartTitle', - { defaultMessage: 'Failed transaction rate' } - )} -

    -
    - -
    -
    -
    +
    diff --git a/x-pack/plugins/apm/public/components/app/backend_operation_detail_view/index.tsx b/x-pack/plugins/apm/public/components/app/backend_operation_detail_view/index.tsx new file mode 100644 index 0000000000000..8378606ab9bc6 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/backend_operation_detail_view/index.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; +import { useApmParams } from '../../../hooks/use_apm_params'; +import { useApmRouter } from '../../../hooks/use_apm_router'; +import { useBackendDetailOperationsBreadcrumb } from '../../../hooks/use_backend_detail_operations_breadcrumb'; +import { BackendMetricCharts } from '../../shared/backend_metric_charts'; +import { DetailViewHeader } from '../../shared/detail_view_header'; + +export function BackendOperationDetailView() { + const router = useApmRouter(); + + const { + query: { spanName, ...query }, + } = useApmParams('/backends/operation'); + + useBackendDetailOperationsBreadcrumb(); + + return ( + + + + + + + + + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/routing/home/index.tsx b/x-pack/plugins/apm/public/components/routing/home/index.tsx index 3512a44acab45..a4caf0f3618b9 100644 --- a/x-pack/plugins/apm/public/components/routing/home/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/home/index.tsx @@ -30,6 +30,8 @@ import { BackendDetailOperations } from '../../app/backend_detail_operations'; import { BackendDetailView } from '../../app/backend_detail_view'; import { RedirectPathBackendDetailView } from './redirect_path_backend_detail_view'; import { RedirectBackendsToBackendDetailOverview } from './redirect_backends_to_backend_detail_view'; +import { BackendOperationDetailView } from '../../app/backend_operation_detail_view'; +import { TimeRangeMetadataContextProvider } from '../../../context/time_range_metadata/time_range_metadata_context'; function page< TPath extends string, @@ -69,6 +71,7 @@ function page< ), children, + params, }, } as any; } @@ -149,7 +152,11 @@ export const DependenciesOperationsTitle = i18n.translate( export const home = { '/': { - element: , + element: ( + + + + ), params: t.type({ query: t.intersection([ environmentRt, @@ -273,16 +280,15 @@ export const home = { children: { '/backends/operations': { element: , - children: { - '/backends/operation': { - params: t.type({ - query: t.type({ - spanName: t.string, - }), - }), - element: , - }, - }, + }, + + '/backends/operation': { + params: t.type({ + query: t.type({ + spanName: t.string, + }), + }), + element: , }, '/backends/overview': { element: , diff --git a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx index 0abbd85dd03a6..11763bf5f155b 100644 --- a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx @@ -29,6 +29,7 @@ import { ServiceLogs } from '../../app/service_logs'; import { InfraOverview } from '../../app/infra_overview'; import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; import { offsetRt } from '../../../../common/comparison_rt'; +import { TimeRangeMetadataContextProvider } from '../../../context/time_range_metadata/time_range_metadata_context'; function page({ title, @@ -63,7 +64,11 @@ function page({ export const serviceDetail = { '/services/{serviceName}': { - element: , + element: ( + + + + ), params: t.intersection([ t.type({ path: t.type({ diff --git a/x-pack/plugins/apm/public/components/routing/templates/backend_detail_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/backend_detail_template.tsx index e36768b8e4482..1ed738cba2e1f 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/backend_detail_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/backend_detail_template.tsx @@ -87,7 +87,8 @@ export function BackendDetailTemplate({ children }: Props) { label: i18n.translate('xpack.apm.backendDetailOperations.title', { defaultMessage: 'Operations', }), - isSelected: path === '/backends/operations', + isSelected: + path === '/backends/operations' || path === '/backends/operation', }, ] : []; diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_error_rate_chart.tsx b/x-pack/plugins/apm/public/components/shared/backend_metric_charts/backend_error_rate_chart.tsx similarity index 72% rename from x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_error_rate_chart.tsx rename to x-pack/plugins/apm/public/components/shared/backend_metric_charts/backend_error_rate_chart.tsx index de02e43852503..1d8834157f4ca 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_error_rate_chart.tsx +++ b/x-pack/plugins/apm/public/components/shared/backend_metric_charts/backend_error_rate_chart.tsx @@ -7,18 +7,19 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { usePreviousPeriodLabel } from '../../../hooks/use_previous_period_text'; -import { isTimeComparison } from '../../shared/time_comparison/get_comparison_options'; +import { isTimeComparison } from '../time_comparison/get_comparison_options'; import { asPercent } from '../../../../common/utils/formatters'; import { useFetcher } from '../../../hooks/use_fetcher'; import { useTimeRange } from '../../../hooks/use_time_range'; import { Coordinate, TimeSeries } from '../../../../typings/timeseries'; -import { TimeseriesChart } from '../../shared/charts/timeseries_chart'; -import { useApmParams } from '../../../hooks/use_apm_params'; +import { TimeseriesChart } from '../charts/timeseries_chart'; import { ChartType, getTimeSeriesColor, -} from '../../shared/charts/helper/get_timeseries_color'; -import { getComparisonChartTheme } from '../../shared/time_comparison/get_comparison_chart_theme'; +} from '../charts/helper/get_timeseries_color'; +import { getComparisonChartTheme } from '../time_comparison/get_comparison_chart_theme'; +import { BackendMetricChartsRouteParams } from './backend_metric_charts_route_params'; +import { useSearchServiceDestinationMetrics } from '../../../context/time_range_metadata/use_search_service_destination_metrics'; function yLabelFormat(y?: number | null) { return asPercent(y || 0, 1); @@ -26,28 +27,27 @@ function yLabelFormat(y?: number | null) { export function BackendFailedTransactionRateChart({ height, + backendName, + kuery, + environment, + rangeFrom, + rangeTo, + offset, + comparisonEnabled, + spanName, }: { height: number; -}) { - const { - query: { - backendName, - kuery, - environment, - rangeFrom, - rangeTo, - offset, - comparisonEnabled, - }, - } = useApmParams('/backends/overview'); - +} & BackendMetricChartsRouteParams) { const { start, end } = useTimeRange({ rangeFrom, rangeTo }); const comparisonChartTheme = getComparisonChartTheme(); + const { isTimeRangeMetadataLoading, searchServiceDestinationMetrics } = + useSearchServiceDestinationMetrics({ rangeFrom, rangeTo, kuery }); + const { data, status } = useFetcher( (callApmApi) => { - if (!start || !end) { + if (isTimeRangeMetadataLoading) { return; } @@ -63,11 +63,24 @@ export function BackendFailedTransactionRateChart({ : undefined, kuery, environment, + spanName: spanName || '', + searchServiceDestinationMetrics, }, }, }); }, - [backendName, start, end, offset, kuery, environment, comparisonEnabled] + [ + backendName, + start, + end, + offset, + kuery, + environment, + comparisonEnabled, + spanName, + isTimeRangeMetadataLoading, + searchServiceDestinationMetrics, + ] ); const { currentPeriodColor, previousPeriodColor } = getTimeSeriesColor( diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_latency_chart.tsx b/x-pack/plugins/apm/public/components/shared/backend_metric_charts/backend_latency_chart.tsx similarity index 69% rename from x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_latency_chart.tsx rename to x-pack/plugins/apm/public/components/shared/backend_metric_charts/backend_latency_chart.tsx index af22fdf2b0c64..03a6fbc81e0fb 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_latency_chart.tsx +++ b/x-pack/plugins/apm/public/components/shared/backend_metric_charts/backend_latency_chart.tsx @@ -7,43 +7,45 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { usePreviousPeriodLabel } from '../../../hooks/use_previous_period_text'; -import { isTimeComparison } from '../../shared/time_comparison/get_comparison_options'; +import { isTimeComparison } from '../time_comparison/get_comparison_options'; import { getDurationFormatter } from '../../../../common/utils/formatters'; import { useFetcher } from '../../../hooks/use_fetcher'; import { useTimeRange } from '../../../hooks/use_time_range'; import { Coordinate, TimeSeries } from '../../../../typings/timeseries'; -import { TimeseriesChart } from '../../shared/charts/timeseries_chart'; +import { TimeseriesChart } from '../charts/timeseries_chart'; import { getMaxY, getResponseTimeTickFormatter, -} from '../../shared/charts/transaction_charts/helper'; -import { useApmParams } from '../../../hooks/use_apm_params'; +} from '../charts/transaction_charts/helper'; import { ChartType, getTimeSeriesColor, -} from '../../shared/charts/helper/get_timeseries_color'; -import { getComparisonChartTheme } from '../../shared/time_comparison/get_comparison_chart_theme'; - -export function BackendLatencyChart({ height }: { height: number }) { - const { - query: { - backendName, - rangeFrom, - rangeTo, - kuery, - environment, - offset, - comparisonEnabled, - }, - } = useApmParams('/backends/overview'); +} from '../charts/helper/get_timeseries_color'; +import { getComparisonChartTheme } from '../time_comparison/get_comparison_chart_theme'; +import { BackendMetricChartsRouteParams } from './backend_metric_charts_route_params'; +import { useSearchServiceDestinationMetrics } from '../../../context/time_range_metadata/use_search_service_destination_metrics'; +export function BackendLatencyChart({ + height, + backendName, + rangeFrom, + rangeTo, + kuery, + environment, + offset, + comparisonEnabled, + spanName, +}: { height: number } & BackendMetricChartsRouteParams) { const { start, end } = useTimeRange({ rangeFrom, rangeTo }); const comparisonChartTheme = getComparisonChartTheme(); + const { isTimeRangeMetadataLoading, searchServiceDestinationMetrics } = + useSearchServiceDestinationMetrics({ rangeFrom, rangeTo, kuery }); + const { data, status } = useFetcher( (callApmApi) => { - if (!start || !end) { + if (isTimeRangeMetadataLoading) { return; } @@ -59,11 +61,24 @@ export function BackendLatencyChart({ height }: { height: number }) { : undefined, kuery, environment, + spanName: spanName || '', + searchServiceDestinationMetrics, }, }, }); }, - [backendName, start, end, offset, kuery, environment, comparisonEnabled] + [ + backendName, + start, + end, + offset, + kuery, + environment, + comparisonEnabled, + spanName, + isTimeRangeMetadataLoading, + searchServiceDestinationMetrics, + ] ); const { currentPeriodColor, previousPeriodColor } = getTimeSeriesColor( diff --git a/x-pack/plugins/apm/public/components/shared/backend_metric_charts/backend_metric_charts_route_params.ts b/x-pack/plugins/apm/public/components/shared/backend_metric_charts/backend_metric_charts_route_params.ts new file mode 100644 index 0000000000000..7c947fe81db06 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/backend_metric_charts/backend_metric_charts_route_params.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { TypeOf } from '@kbn/typed-react-router-config'; +import { ApmRoutes } from '../../routing/apm_route_config'; + +export type BackendMetricChartsRouteParams = Pick< + { spanName?: string } & TypeOf< + ApmRoutes, + '/backends/operation' | '/backends/overview' + >['query'], + | 'backendName' + | 'comparisonEnabled' + | 'spanName' + | 'rangeFrom' + | 'rangeTo' + | 'kuery' + | 'environment' + | 'comparisonEnabled' + | 'offset' +>; diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_throughput_chart.tsx b/x-pack/plugins/apm/public/components/shared/backend_metric_charts/backend_throughput_chart.tsx similarity index 69% rename from x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_throughput_chart.tsx rename to x-pack/plugins/apm/public/components/shared/backend_metric_charts/backend_throughput_chart.tsx index 5ea883f65f576..bd461e49eeaf6 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_throughput_chart.tsx +++ b/x-pack/plugins/apm/public/components/shared/backend_metric_charts/backend_throughput_chart.tsx @@ -7,39 +7,41 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { usePreviousPeriodLabel } from '../../../hooks/use_previous_period_text'; -import { isTimeComparison } from '../../shared/time_comparison/get_comparison_options'; +import { isTimeComparison } from '../time_comparison/get_comparison_options'; import { asTransactionRate } from '../../../../common/utils/formatters'; import { useFetcher } from '../../../hooks/use_fetcher'; import { useTimeRange } from '../../../hooks/use_time_range'; import { Coordinate, TimeSeries } from '../../../../typings/timeseries'; -import { TimeseriesChart } from '../../shared/charts/timeseries_chart'; -import { useApmParams } from '../../../hooks/use_apm_params'; +import { TimeseriesChart } from '../charts/timeseries_chart'; import { ChartType, getTimeSeriesColor, -} from '../../shared/charts/helper/get_timeseries_color'; -import { getComparisonChartTheme } from '../../shared/time_comparison/get_comparison_chart_theme'; - -export function BackendThroughputChart({ height }: { height: number }) { - const { - query: { - backendName, - rangeFrom, - rangeTo, - kuery, - environment, - offset, - comparisonEnabled, - }, - } = useApmParams('/backends/overview'); +} from '../charts/helper/get_timeseries_color'; +import { getComparisonChartTheme } from '../time_comparison/get_comparison_chart_theme'; +import { BackendMetricChartsRouteParams } from './backend_metric_charts_route_params'; +import { useSearchServiceDestinationMetrics } from '../../../context/time_range_metadata/use_search_service_destination_metrics'; +export function BackendThroughputChart({ + height, + backendName, + rangeFrom, + rangeTo, + kuery, + environment, + offset, + comparisonEnabled, + spanName, +}: { height: number } & BackendMetricChartsRouteParams) { const { start, end } = useTimeRange({ rangeFrom, rangeTo }); const comparisonChartTheme = getComparisonChartTheme(); + const { isTimeRangeMetadataLoading, searchServiceDestinationMetrics } = + useSearchServiceDestinationMetrics({ rangeFrom, rangeTo, kuery }); + const { data, status } = useFetcher( (callApmApi) => { - if (!start || !end) { + if (isTimeRangeMetadataLoading) { return; } @@ -55,11 +57,24 @@ export function BackendThroughputChart({ height }: { height: number }) { : undefined, kuery, environment, + spanName: spanName || '', + searchServiceDestinationMetrics, }, }, }); }, - [backendName, start, end, offset, kuery, environment, comparisonEnabled] + [ + backendName, + start, + end, + offset, + kuery, + environment, + comparisonEnabled, + spanName, + isTimeRangeMetadataLoading, + searchServiceDestinationMetrics, + ] ); const { currentPeriodColor, previousPeriodColor } = getTimeSeriesColor( diff --git a/x-pack/plugins/apm/public/components/shared/backend_metric_charts/index.tsx b/x-pack/plugins/apm/public/components/shared/backend_metric_charts/index.tsx new file mode 100644 index 0000000000000..c13ddde4ff1fb --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/backend_metric_charts/index.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useAnyOfApmParams } from '../../../hooks/use_apm_params'; +import { useBreakpoints } from '../../../hooks/use_breakpoints'; +import { BackendFailedTransactionRateChart } from './backend_error_rate_chart'; +import { BackendLatencyChart } from './backend_latency_chart'; +import { BackendMetricChartsRouteParams } from './backend_metric_charts_route_params'; +import { BackendThroughputChart } from './backend_throughput_chart'; + +export function BackendMetricCharts() { + const largeScreenOrSmaller = useBreakpoints().isLarge; + + const { + query, + query: { + backendName, + rangeFrom, + rangeTo, + kuery, + environment, + comparisonEnabled, + offset, + }, + } = useAnyOfApmParams('/backends/overview', '/backends/operation'); + + const spanName = 'spanName' in query ? query.spanName : undefined; + + const props: BackendMetricChartsRouteParams = { + backendName, + rangeFrom, + rangeTo, + kuery, + environment, + comparisonEnabled, + offset, + spanName, + }; + + return ( + + + + +

    + {i18n.translate('xpack.apm.backendDetailLatencyChartTitle', { + defaultMessage: 'Latency', + })} +

    +
    + +
    +
    + + + +

    + {i18n.translate('xpack.apm.backendDetailThroughputChartTitle', { + defaultMessage: 'Throughput', + })} +

    +
    + +
    +
    + + + +

    + {i18n.translate( + 'xpack.apm.backendDetailFailedTransactionRateChartTitle', + { defaultMessage: 'Failed transaction rate' } + )} +

    +
    + +
    +
    +
    + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/detail_view_header/index.tsx b/x-pack/plugins/apm/public/components/shared/detail_view_header/index.tsx new file mode 100644 index 0000000000000..2a79f68820454 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/detail_view_header/index.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLink, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +export function DetailViewHeader({ + backLabel, + backHref, + title, +}: { + backLabel: string; + backHref: string; + title: string; +}) { + return ( + + + + + + + + {backLabel} + + + + + + {title} + + + + ); +} diff --git a/x-pack/plugins/apm/public/context/time_range_metadata/time_range_metadata_context.tsx b/x-pack/plugins/apm/public/context/time_range_metadata/time_range_metadata_context.tsx new file mode 100644 index 0000000000000..acb21a20620d0 --- /dev/null +++ b/x-pack/plugins/apm/public/context/time_range_metadata/time_range_metadata_context.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { createContext } from 'react'; +import { TimeRangeMetadata } from '../../../common/time_range_metadata'; +import { useApmParams } from '../../hooks/use_apm_params'; +import { useApmRoutePath } from '../../hooks/use_apm_route_path'; +import { FetcherResult, useFetcher } from '../../hooks/use_fetcher'; +import { useTimeRange } from '../../hooks/use_time_range'; + +export const TimeRangeMetadataContext = createContext< + FetcherResult | undefined +>(undefined); + +export function TimeRangeMetadataContextProvider({ + children, +}: { + children: React.ReactElement; +}) { + const { query } = useApmParams('/*'); + + const kuery = 'kuery' in query ? query.kuery : ''; + + const range = + 'rangeFrom' in query && 'rangeTo' in query + ? { rangeFrom: query.rangeFrom, rangeTo: query.rangeTo } + : undefined; + + if (!range) { + throw new Error('rangeFrom/rangeTo missing in URL'); + } + + const { start, end } = useTimeRange(range); + + const routePath = useApmRoutePath(); + + const isOperationView = + routePath === '/backends/operation' || routePath === '/backends/operations'; + + const fetcherResult = useFetcher( + (callApmApi) => { + return callApmApi('GET /internal/apm/time_range_metadata', { + params: { + query: { + start, + end, + kuery, + useSpanName: isOperationView, + }, + }, + }); + }, + [start, end, kuery, isOperationView] + ); + + return ( + + {children} + + ); +} diff --git a/x-pack/plugins/apm/public/context/time_range_metadata/use_search_service_destination_metrics.ts b/x-pack/plugins/apm/public/context/time_range_metadata/use_search_service_destination_metrics.ts new file mode 100644 index 0000000000000..fa8ae73d81f72 --- /dev/null +++ b/x-pack/plugins/apm/public/context/time_range_metadata/use_search_service_destination_metrics.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FETCH_STATUS } from '../../hooks/use_fetcher'; +import { useTimeRangeMetadata } from './use_time_range_metadata_context'; + +export function useSearchServiceDestinationMetrics({ + rangeFrom, + rangeTo, + kuery, +}: { + rangeFrom: string; + rangeTo: string; + kuery: string; +}) { + const { status, data } = useTimeRangeMetadata({ + rangeFrom, + rangeTo, + kuery, + }); + + return { + isTimeRangeMetadataLoading: status === FETCH_STATUS.LOADING, + searchServiceDestinationMetrics: + data?.isUsingServiceDestinationMetrics ?? true, + }; +} diff --git a/x-pack/plugins/apm/public/context/time_range_metadata/use_time_range_metadata_context.ts b/x-pack/plugins/apm/public/context/time_range_metadata/use_time_range_metadata_context.ts new file mode 100644 index 0000000000000..d23b6ba9de54c --- /dev/null +++ b/x-pack/plugins/apm/public/context/time_range_metadata/use_time_range_metadata_context.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useContext } from 'react'; +import { TimeRangeMetadataContext } from './time_range_metadata_context'; + +export function useTimeRangeMetadata({ + rangeFrom, + rangeTo, + kuery, +}: { + // require parameters to enforce type-safety. Only components + // with access to rangeFrom and rangeTo should be able to request + // time range metadata. + rangeFrom: string; + rangeTo: string; + kuery: string; +}) { + const context = useContext(TimeRangeMetadataContext); + + if (!context) { + throw new Error('TimeRangeMetadataContext is not found'); + } + + return context; +} diff --git a/x-pack/plugins/apm/public/hooks/use_backend_detail_operations_breadcrumb.ts b/x-pack/plugins/apm/public/hooks/use_backend_detail_operations_breadcrumb.ts new file mode 100644 index 0000000000000..c5235333e8426 --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_backend_detail_operations_breadcrumb.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { useBreadcrumb } from '../context/breadcrumbs/use_breadcrumb'; +import { useAnyOfApmParams } from './use_apm_params'; +import { useApmRouter } from './use_apm_router'; + +export function useBackendDetailOperationsBreadcrumb() { + const { + query: { + backendName, + rangeFrom, + rangeTo, + refreshInterval, + refreshPaused, + environment, + kuery, + comparisonEnabled, + }, + } = useAnyOfApmParams('/backends/operations', '/backends/operation'); + + const apmRouter = useApmRouter(); + + useBreadcrumb([ + { + title: i18n.translate( + 'xpack.apm.backendDetailOperations.breadcrumbTitle', + { defaultMessage: 'Operations' } + ), + href: apmRouter.link('/backends/operations', { + query: { + backendName, + rangeFrom, + rangeTo, + refreshInterval, + refreshPaused, + environment, + kuery, + comparisonEnabled, + }, + }), + }, + ]); +} diff --git a/x-pack/plugins/apm/server/lib/helpers/spans/get_is_using_service_destination_metrics.ts b/x-pack/plugins/apm/server/lib/helpers/spans/get_is_using_service_destination_metrics.ts new file mode 100644 index 0000000000000..b3a003836b416 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/spans/get_is_using_service_destination_metrics.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + kqlQuery, + rangeQuery, + termQuery, +} from '@kbn/observability-plugin/server'; +import { + METRICSET_NAME, + SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, + SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM, + SPAN_DURATION, + SPAN_NAME, +} from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { Setup } from '../setup_request'; + +export function getProcessorEventForServiceDestinationStatistics( + searchServiceDestinationMetrics: boolean +) { + return searchServiceDestinationMetrics + ? ProcessorEvent.metric + : ProcessorEvent.span; +} + +export function getDocumentTypeFilterForServiceDestinationStatistics( + searchServiceDestinationMetrics: boolean +) { + return searchServiceDestinationMetrics + ? termQuery(METRICSET_NAME, 'service_destination') + : []; +} + +export function getLatencyFieldForServiceDestinationStatistics( + searchServiceDestinationMetrics: boolean +) { + return searchServiceDestinationMetrics + ? SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM + : SPAN_DURATION; +} + +export function getDocCountFieldForServiceDestinationStatistics( + searchServiceDestinationMetrics: boolean +) { + return searchServiceDestinationMetrics + ? SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT + : undefined; +} + +export async function getIsUsingServiceDestinationMetrics({ + setup, + useSpanName, + kuery, + start, + end, +}: { + setup: Setup; + useSpanName: boolean; + kuery: string; + start: number; + end: number; +}) { + const { apmEventClient } = setup; + + const response = await apmEventClient.search( + 'get_has_service_destination_metrics', + { + apm: { + events: [getProcessorEventForServiceDestinationStatistics(true)], + }, + body: { + terminate_after: 1, + size: 1, + query: { + bool: { + filter: [ + ...rangeQuery(start, end), + ...kqlQuery(kuery), + ...getDocumentTypeFilterForServiceDestinationStatistics(true), + ...(useSpanName ? [{ exists: { field: SPAN_NAME } }] : []), + ], + }, + }, + }, + } + ); + + return response.hits.total.value > 0; +} diff --git a/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts index ae5ea60b4e58b..4c6bc38192fa4 100644 --- a/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts +++ b/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts @@ -5,24 +5,28 @@ * 2.0. */ import type { - ServerRouteRepository, - ReturnOf, EndpointOf, + ReturnOf, + ServerRouteRepository, } from '@kbn/server-route-repository'; import { PickByValue } from 'utility-types'; -import { correlationsRouteRepository } from '../correlations/route'; +import { agentKeysRouteRepository } from '../agent_keys/route'; import { alertsChartPreviewRouteRepository } from '../alerts/route'; import { backendsRouteRepository } from '../backends/route'; +import { correlationsRouteRepository } from '../correlations/route'; +import { dataViewRouteRepository } from '../data_view/route'; +import { debugTelemetryRoute } from '../debug_telemetry/route'; import { environmentsRouteRepository } from '../environments/route'; import { errorsRouteRepository } from '../errors/route'; -import { infrastructureRouteRepository } from '../infrastructure/route'; +import { eventMetadataRouteRepository } from '../event_metadata/route'; +import { fallbackToTransactionsRouteRepository } from '../fallback_to_transactions/route'; import { apmFleetRouteRepository } from '../fleet/route'; -import { dataViewRouteRepository } from '../data_view/route'; +import { historicalDataRouteRepository } from '../historical_data/route'; +import { infrastructureRouteRepository } from '../infrastructure/route'; import { latencyDistributionRouteRepository } from '../latency_distribution/route'; import { metricsRouteRepository } from '../metrics/route'; import { observabilityOverviewRouteRepository } from '../observability_overview/route'; import { rumRouteRepository } from '../rum_client/route'; -import { fallbackToTransactionsRouteRepository } from '../fallback_to_transactions/route'; import { serviceRouteRepository } from '../services/route'; import { serviceGroupRouteRepository } from '../service_groups/route'; import { serviceMapRouteRepository } from '../service_map/route'; @@ -32,14 +36,12 @@ import { anomalyDetectionRouteRepository } from '../settings/anomaly_detection/r import { apmIndicesRouteRepository } from '../settings/apm_indices/route'; import { customLinkRouteRepository } from '../settings/custom_link/route'; import { sourceMapsRouteRepository } from '../source_maps/route'; +import { spanLinksRouteRepository } from '../span_links/route'; +import { suggestionsRouteRepository } from '../suggestions/route'; +import { timeRangeMetadataRoute } from '../time_range_metadata/route'; import { traceRouteRepository } from '../traces/route'; import { transactionRouteRepository } from '../transactions/route'; -import { historicalDataRouteRepository } from '../historical_data/route'; -import { eventMetadataRouteRepository } from '../event_metadata/route'; -import { suggestionsRouteRepository } from '../suggestions/route'; -import { agentKeysRouteRepository } from '../agent_keys/route'; -import { spanLinksRouteRepository } from '../span_links/route'; -import { debugTelemetryRoute } from '../debug_telemetry/route'; + function getTypedGlobalApmServerRouteRepository() { const repository = { ...dataViewRouteRepository, @@ -72,6 +74,7 @@ function getTypedGlobalApmServerRouteRepository() { ...spanLinksRouteRepository, ...infrastructureRouteRepository, ...debugTelemetryRoute, + ...timeRangeMetadataRoute, }; return repository; diff --git a/x-pack/plugins/apm/server/routes/backends/get_error_rate_charts_for_backend.ts b/x-pack/plugins/apm/server/routes/backends/get_error_rate_charts_for_backend.ts index 52857a4c77367..a1890e103d412 100644 --- a/x-pack/plugins/apm/server/routes/backends/get_error_rate_charts_for_backend.ts +++ b/x-pack/plugins/apm/server/routes/backends/get_error_rate_charts_for_backend.ts @@ -5,33 +5,46 @@ * 2.0. */ -import { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server'; +import { + kqlQuery, + rangeQuery, + termQuery, +} from '@kbn/observability-plugin/server'; import { EventOutcome } from '../../../common/event_outcome'; import { EVENT_OUTCOME, SPAN_DESTINATION_SERVICE_RESOURCE, + SPAN_NAME, } from '../../../common/elasticsearch_fieldnames'; import { environmentQuery } from '../../../common/utils/environment_query'; -import { ProcessorEvent } from '../../../common/processor_event'; import { Setup } from '../../lib/helpers/setup_request'; import { getMetricsDateHistogramParams } from '../../lib/helpers/metrics'; import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms'; +import { + getDocCountFieldForServiceDestinationStatistics, + getDocumentTypeFilterForServiceDestinationStatistics, + getProcessorEventForServiceDestinationStatistics, +} from '../../lib/helpers/spans/get_is_using_service_destination_metrics'; export async function getErrorRateChartsForBackend({ backendName, + spanName, setup, start, end, environment, kuery, + searchServiceDestinationMetrics, offset, }: { backendName: string; + spanName: string; setup: Setup; start: number; end: number; environment: string; kuery: string; + searchServiceDestinationMetrics: boolean; offset?: string; }) { const { apmEventClient } = setup; @@ -44,7 +57,11 @@ export async function getErrorRateChartsForBackend({ const response = await apmEventClient.search('get_error_rate_for_backend', { apm: { - events: [ProcessorEvent.metric], + events: [ + getProcessorEventForServiceDestinationStatistics( + searchServiceDestinationMetrics + ), + ], }, body: { size: 0, @@ -54,6 +71,10 @@ export async function getErrorRateChartsForBackend({ ...environmentQuery(environment), ...kqlQuery(kuery), ...rangeQuery(startWithOffset, endWithOffset), + ...termQuery(SPAN_NAME, spanName || null), + ...getDocumentTypeFilterForServiceDestinationStatistics( + searchServiceDestinationMetrics + ), { term: { [SPAN_DESTINATION_SERVICE_RESOURCE]: backendName } }, { terms: { @@ -71,12 +92,37 @@ export async function getErrorRateChartsForBackend({ metricsInterval: 60, }), aggs: { + ...(searchServiceDestinationMetrics + ? { + total_count: { + sum: { + field: getDocCountFieldForServiceDestinationStatistics( + searchServiceDestinationMetrics + ), + }, + }, + } + : {}), failures: { filter: { term: { [EVENT_OUTCOME]: EventOutcome.failure, }, }, + aggs: { + ...(searchServiceDestinationMetrics + ? { + total_count: { + sum: { + field: + getDocCountFieldForServiceDestinationStatistics( + searchServiceDestinationMetrics + ), + }, + }, + } + : {}), + }, }, }, }, @@ -86,8 +132,9 @@ export async function getErrorRateChartsForBackend({ return ( response.aggregations?.timeseries.buckets.map((bucket) => { - const totalCount = bucket.doc_count; - const failureCount = bucket.failures.doc_count; + const totalCount = bucket.total_count?.value ?? bucket.doc_count; + const failureCount = + bucket.failures.total_count?.value ?? bucket.failures.doc_count; return { x: bucket.key + offsetInMs, diff --git a/x-pack/plugins/apm/server/routes/backends/get_latency_charts_for_backend.ts b/x-pack/plugins/apm/server/routes/backends/get_latency_charts_for_backend.ts index 3ed74c7af713c..1dfe61904112c 100644 --- a/x-pack/plugins/apm/server/routes/backends/get_latency_charts_for_backend.ts +++ b/x-pack/plugins/apm/server/routes/backends/get_latency_charts_for_backend.ts @@ -5,20 +5,30 @@ * 2.0. */ -import { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server'; +import { + kqlQuery, + rangeQuery, + termQuery, +} from '@kbn/observability-plugin/server'; import { SPAN_DESTINATION_SERVICE_RESOURCE, - SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, - SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM, + SPAN_NAME, } from '../../../common/elasticsearch_fieldnames'; import { environmentQuery } from '../../../common/utils/environment_query'; -import { ProcessorEvent } from '../../../common/processor_event'; import { Setup } from '../../lib/helpers/setup_request'; import { getMetricsDateHistogramParams } from '../../lib/helpers/metrics'; import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms'; +import { + getDocCountFieldForServiceDestinationStatistics, + getDocumentTypeFilterForServiceDestinationStatistics, + getLatencyFieldForServiceDestinationStatistics, + getProcessorEventForServiceDestinationStatistics, +} from '../../lib/helpers/spans/get_is_using_service_destination_metrics'; export async function getLatencyChartsForBackend({ backendName, + spanName, + searchServiceDestinationMetrics, setup, start, end, @@ -27,6 +37,8 @@ export async function getLatencyChartsForBackend({ offset, }: { backendName: string; + spanName: string; + searchServiceDestinationMetrics: boolean; setup: Setup; start: number; end: number; @@ -44,7 +56,11 @@ export async function getLatencyChartsForBackend({ const response = await apmEventClient.search('get_latency_for_backend', { apm: { - events: [ProcessorEvent.metric], + events: [ + getProcessorEventForServiceDestinationStatistics( + searchServiceDestinationMetrics + ), + ], }, body: { size: 0, @@ -54,6 +70,10 @@ export async function getLatencyChartsForBackend({ ...environmentQuery(environment), ...kqlQuery(kuery), ...rangeQuery(startWithOffset, endWithOffset), + ...termQuery(SPAN_NAME, spanName || null), + ...getDocumentTypeFilterForServiceDestinationStatistics( + searchServiceDestinationMetrics + ), { term: { [SPAN_DESTINATION_SERVICE_RESOURCE]: backendName } }, ], }, @@ -68,14 +88,22 @@ export async function getLatencyChartsForBackend({ aggs: { latency_sum: { sum: { - field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM, - }, - }, - latency_count: { - sum: { - field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, + field: getLatencyFieldForServiceDestinationStatistics( + searchServiceDestinationMetrics + ), }, }, + ...(searchServiceDestinationMetrics + ? { + latency_count: { + sum: { + field: getDocCountFieldForServiceDestinationStatistics( + searchServiceDestinationMetrics + ), + }, + }, + } + : {}), }, }, }, @@ -86,7 +114,9 @@ export async function getLatencyChartsForBackend({ response.aggregations?.timeseries.buckets.map((bucket) => { return { x: bucket.key + offsetInMs, - y: (bucket.latency_sum.value ?? 0) / (bucket.latency_count.value ?? 0), + y: + (bucket.latency_sum.value ?? 0) / + (bucket.latency_count?.value ?? bucket.doc_count), }; }) ?? [] ); diff --git a/x-pack/plugins/apm/server/routes/backends/get_throughput_charts_for_backend.ts b/x-pack/plugins/apm/server/routes/backends/get_throughput_charts_for_backend.ts index 3544faa2ebad8..55930ad918646 100644 --- a/x-pack/plugins/apm/server/routes/backends/get_throughput_charts_for_backend.ts +++ b/x-pack/plugins/apm/server/routes/backends/get_throughput_charts_for_backend.ts @@ -5,32 +5,44 @@ * 2.0. */ -import { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server'; +import { + kqlQuery, + rangeQuery, + termQuery, +} from '@kbn/observability-plugin/server'; import { SPAN_DESTINATION_SERVICE_RESOURCE, - SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, + SPAN_NAME, } from '../../../common/elasticsearch_fieldnames'; import { environmentQuery } from '../../../common/utils/environment_query'; -import { ProcessorEvent } from '../../../common/processor_event'; import { Setup } from '../../lib/helpers/setup_request'; import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms'; import { getBucketSize } from '../../lib/helpers/get_bucket_size'; +import { + getDocCountFieldForServiceDestinationStatistics, + getDocumentTypeFilterForServiceDestinationStatistics, + getProcessorEventForServiceDestinationStatistics, +} from '../../lib/helpers/spans/get_is_using_service_destination_metrics'; export async function getThroughputChartsForBackend({ backendName, + spanName, setup, start, end, environment, kuery, + searchServiceDestinationMetrics, offset, }: { backendName: string; + spanName: string; setup: Setup; start: number; end: number; environment: string; kuery: string; + searchServiceDestinationMetrics: boolean; offset?: string; }) { const { apmEventClient } = setup; @@ -49,7 +61,11 @@ export async function getThroughputChartsForBackend({ const response = await apmEventClient.search('get_throughput_for_backend', { apm: { - events: [ProcessorEvent.metric], + events: [ + getProcessorEventForServiceDestinationStatistics( + searchServiceDestinationMetrics + ), + ], }, body: { size: 0, @@ -59,6 +75,10 @@ export async function getThroughputChartsForBackend({ ...environmentQuery(environment), ...kqlQuery(kuery), ...rangeQuery(startWithOffset, endWithOffset), + ...termQuery(SPAN_NAME, spanName || null), + ...getDocumentTypeFilterForServiceDestinationStatistics( + searchServiceDestinationMetrics + ), { term: { [SPAN_DESTINATION_SERVICE_RESOURCE]: backendName } }, ], }, @@ -74,7 +94,13 @@ export async function getThroughputChartsForBackend({ aggs: { throughput: { rate: { - field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, + ...(searchServiceDestinationMetrics + ? { + field: getDocCountFieldForServiceDestinationStatistics( + searchServiceDestinationMetrics + ), + } + : {}), unit: 'minute', }, }, diff --git a/x-pack/plugins/apm/server/routes/backends/route.ts b/x-pack/plugins/apm/server/routes/backends/route.ts index 120f9274a5127..2c1a4fc8a6222 100644 --- a/x-pack/plugins/apm/server/routes/backends/route.ts +++ b/x-pack/plugins/apm/server/routes/backends/route.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { toNumberRt } from '@kbn/io-ts-utils'; +import { toBooleanRt, toNumberRt } from '@kbn/io-ts-utils'; import { setupRequest } from '../../lib/helpers/setup_request'; import { environmentRt, kueryRt, rangeRt } from '../default_api_types'; import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; @@ -278,7 +278,11 @@ const backendLatencyChartsRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/backends/charts/latency', params: t.type({ query: t.intersection([ - t.type({ backendName: t.string }), + t.type({ + backendName: t.string, + spanName: t.string, + searchServiceDestinationMetrics: toBooleanRt, + }), rangeRt, kueryRt, environmentRt, @@ -296,12 +300,22 @@ const backendLatencyChartsRoute = createApmServerRoute({ }> => { const setup = await setupRequest(resources); const { params } = resources; - const { backendName, kuery, environment, offset, start, end } = - params.query; + const { + backendName, + searchServiceDestinationMetrics, + spanName, + kuery, + environment, + offset, + start, + end, + } = params.query; const [currentTimeseries, comparisonTimeseries] = await Promise.all([ getLatencyChartsForBackend({ backendName, + spanName, + searchServiceDestinationMetrics, setup, start, end, @@ -311,6 +325,8 @@ const backendLatencyChartsRoute = createApmServerRoute({ offset ? getLatencyChartsForBackend({ backendName, + spanName, + searchServiceDestinationMetrics, setup, start, end, @@ -329,7 +345,11 @@ const backendThroughputChartsRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/backends/charts/throughput', params: t.type({ query: t.intersection([ - t.type({ backendName: t.string }), + t.type({ + backendName: t.string, + spanName: t.string, + searchServiceDestinationMetrics: toBooleanRt, + }), rangeRt, kueryRt, environmentRt, @@ -347,27 +367,39 @@ const backendThroughputChartsRoute = createApmServerRoute({ }> => { const setup = await setupRequest(resources); const { params } = resources; - const { backendName, kuery, environment, offset, start, end } = - params.query; + const { + backendName, + searchServiceDestinationMetrics, + spanName, + kuery, + environment, + offset, + start, + end, + } = params.query; const [currentTimeseries, comparisonTimeseries] = await Promise.all([ getThroughputChartsForBackend({ backendName, + spanName, setup, start, end, kuery, environment, + searchServiceDestinationMetrics, }), offset ? getThroughputChartsForBackend({ backendName, + spanName, setup, start, end, kuery, environment, offset, + searchServiceDestinationMetrics, }) : null, ]); @@ -380,7 +412,11 @@ const backendFailedTransactionRateChartsRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/backends/charts/error_rate', params: t.type({ query: t.intersection([ - t.type({ backendName: t.string }), + t.type({ + backendName: t.string, + spanName: t.string, + searchServiceDestinationMetrics: toBooleanRt, + }), rangeRt, kueryRt, environmentRt, @@ -398,27 +434,39 @@ const backendFailedTransactionRateChartsRoute = createApmServerRoute({ }> => { const setup = await setupRequest(resources); const { params } = resources; - const { backendName, kuery, environment, offset, start, end } = - params.query; + const { + backendName, + spanName, + searchServiceDestinationMetrics, + kuery, + environment, + offset, + start, + end, + } = params.query; const [currentTimeseries, comparisonTimeseries] = await Promise.all([ getErrorRateChartsForBackend({ backendName, + spanName, setup, start, end, kuery, environment, + searchServiceDestinationMetrics, }), offset ? getErrorRateChartsForBackend({ backendName, + spanName, setup, start, end, kuery, environment, offset, + searchServiceDestinationMetrics, }) : null, ]); diff --git a/x-pack/plugins/apm/server/routes/time_range_metadata/route.ts b/x-pack/plugins/apm/server/routes/time_range_metadata/route.ts new file mode 100644 index 0000000000000..f0321f4cfde4d --- /dev/null +++ b/x-pack/plugins/apm/server/routes/time_range_metadata/route.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { toBooleanRt } from '@kbn/io-ts-utils'; +import * as t from 'io-ts'; +import { TimeRangeMetadata } from '../../../common/time_range_metadata'; +import { setupRequest } from '../../lib/helpers/setup_request'; +import { getIsUsingServiceDestinationMetrics } from '../../lib/helpers/spans/get_is_using_service_destination_metrics'; +import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; +import { kueryRt, rangeRt } from '../default_api_types'; + +export const timeRangeMetadataRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/time_range_metadata', + params: t.type({ + query: t.intersection([ + t.type({ useSpanName: toBooleanRt }), + kueryRt, + rangeRt, + ]), + }), + options: { + tags: ['access:apm'], + }, + handler: async (resources): Promise => { + const setup = await setupRequest(resources); + + const { + query: { useSpanName, start, end, kuery }, + } = resources.params; + + const [isUsingServiceDestinationMetrics] = await Promise.all([ + getIsUsingServiceDestinationMetrics({ + setup, + useSpanName, + start, + end, + kuery, + }), + ]); + + return { + isUsingServiceDestinationMetrics, + }; + }, +}); diff --git a/x-pack/test/apm_api_integration/tests/dependencies/dependency_metrics.spec.ts b/x-pack/test/apm_api_integration/tests/dependencies/dependency_metrics.spec.ts new file mode 100644 index 0000000000000..fb9f1ed9485e0 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/dependencies/dependency_metrics.spec.ts @@ -0,0 +1,308 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import { sum } from 'lodash'; +import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number'; +import { Coordinate } from '@kbn/apm-plugin/typings/timeseries'; +import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { roundNumber } from '../../utils'; +import { generateOperationData, generateOperationDataConfig } from './generate_operation_data'; +import { SupertestReturnType } from '../../common/apm_api_supertest'; + +const { + ES_BULK_DURATION, + ES_BULK_RATE, + ES_SEARCH_DURATION, + ES_SEARCH_FAILURE_RATE, + ES_SEARCH_SUCCESS_RATE, + ES_SEARCH_UNKNOWN_RATE, + REDIS_SET_RATE, +} = generateOperationDataConfig; + +export default function ApiTest({ getService }: FtrProviderContext) { + const registry = getService('registry'); + const apmApiClient = getService('apmApiClient'); + const synthtraceEsClient = getService('synthtraceEsClient'); + + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + + async function callApi({ + backendName, + searchServiceDestinationMetrics, + spanName = '', + metric, + kuery = '', + environment = ENVIRONMENT_ALL.value, + }: { + backendName: string; + searchServiceDestinationMetrics: boolean; + spanName?: string; + metric: TMetricName; + kuery?: string; + environment?: string; + }): Promise> { + return await apmApiClient.readUser({ + endpoint: `GET /internal/apm/backends/charts/${ + metric as 'latency' | 'throughput' | 'error_rate' + }`, + params: { + query: { + backendName, + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + environment, + kuery, + offset: '', + spanName, + searchServiceDestinationMetrics, + }, + }, + }); + } + + function avg(coordinates: Coordinate[]) { + const values = coordinates + .filter((coord): coord is { x: number; y: number } => isFiniteNumber(coord.y)) + .map((coord) => coord.y); + + return roundNumber(sum(values) / values.length); + } + + registry.when( + 'Dependency metrics when data is not loaded', + { config: 'basic', archives: [] }, + () => { + it('handles empty state', async () => { + const { body, status } = await callApi({ + backendName: 'elasticsearch', + metric: 'latency', + searchServiceDestinationMetrics: true, + }); + + expect(status).to.be(200); + expect(body.currentTimeseries.filter((val) => isFiniteNumber(val.y))).to.empty(); + expect( + (body.comparisonTimeseries || [])?.filter((val) => isFiniteNumber(val.y)) + ).to.empty(); + }); + } + ); + + registry.when( + 'Dependency metrics when data is loaded', + { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, + () => { + before(async () => { + await generateOperationData({ + synthtraceEsClient, + start, + end, + }); + }); + + describe('without spanName', () => { + describe('without a kuery or environment', () => { + it('returns the correct latency', async () => { + const response = await callApi({ + backendName: 'elasticsearch', + searchServiceDestinationMetrics: true, + spanName: '', + metric: 'latency', + }); + + const searchRate = + ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_UNKNOWN_RATE; + const bulkRate = ES_BULK_RATE; + + expect(avg(response.body.currentTimeseries)).to.eql( + roundNumber( + ((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) / + (searchRate + bulkRate)) * + 1000 + ) + ); + }); + + it('returns the correct throughput', async () => { + const response = await callApi({ + backendName: 'redis', + searchServiceDestinationMetrics: true, + spanName: '', + metric: 'throughput', + }); + + expect(avg(response.body.currentTimeseries)).to.eql(REDIS_SET_RATE); + }); + + it('returns the correct failure rate', async () => { + const response = await callApi({ + backendName: 'elasticsearch', + searchServiceDestinationMetrics: true, + spanName: '', + metric: 'error_rate', + }); + + const expectedErrorRate = + ES_SEARCH_FAILURE_RATE / (ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE); + + expect(avg(response.body.currentTimeseries)).to.eql(expectedErrorRate); + }); + }); + + describe('with a kuery', () => { + it('returns the correct latency', async () => { + const response = await callApi({ + backendName: 'elasticsearch', + searchServiceDestinationMetrics: true, + spanName: '', + metric: 'latency', + kuery: `event.outcome:unknown`, + }); + + const searchRate = ES_SEARCH_UNKNOWN_RATE; + const bulkRate = ES_BULK_RATE; + + expect(avg(response.body.currentTimeseries)).to.eql( + roundNumber( + ((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) / + (searchRate + bulkRate)) * + 1000 + ) + ); + }); + + it('returns the correct throughput', async () => { + const response = await callApi({ + backendName: 'elasticsearch', + searchServiceDestinationMetrics: true, + spanName: '', + metric: 'throughput', + kuery: `event.outcome:unknown`, + }); + + const searchRate = ES_SEARCH_UNKNOWN_RATE; + const bulkRate = ES_BULK_RATE; + + expect(avg(response.body.currentTimeseries)).to.eql(roundNumber(searchRate + bulkRate)); + }); + + it('returns the correct failure rate', async () => { + const response = await callApi({ + backendName: 'elasticsearch', + searchServiceDestinationMetrics: true, + spanName: '', + metric: 'error_rate', + kuery: 'event.outcome:success', + }); + + expect(avg(response.body.currentTimeseries)).to.eql(0); + }); + }); + + describe('with an environment', () => { + it('returns the correct latency', async () => { + const response = await callApi({ + backendName: 'elasticsearch', + searchServiceDestinationMetrics: true, + spanName: '', + metric: 'latency', + environment: 'production', + }); + + const searchRate = ES_SEARCH_UNKNOWN_RATE; + const bulkRate = 0; + + expect(avg(response.body.currentTimeseries)).to.eql( + roundNumber( + ((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) / + (searchRate + bulkRate)) * + 1000 + ) + ); + }); + + it('returns the correct throughput', async () => { + const response = await callApi({ + backendName: 'elasticsearch', + searchServiceDestinationMetrics: true, + spanName: '', + metric: 'throughput', + environment: 'production', + }); + + const searchRate = + ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_UNKNOWN_RATE; + const bulkRate = 0; + + expect(avg(response.body.currentTimeseries)).to.eql(roundNumber(searchRate + bulkRate)); + }); + + it('returns the correct failure rate', async () => { + const response = await callApi({ + backendName: 'elasticsearch', + searchServiceDestinationMetrics: true, + spanName: '', + metric: 'error_rate', + environment: 'development', + }); + + expect(avg(response.body.currentTimeseries)).to.eql(null); + }); + }); + }); + + describe('with spanName', () => { + it('returns the correct latency', async () => { + const response = await callApi({ + backendName: 'elasticsearch', + searchServiceDestinationMetrics: false, + spanName: '/_search', + metric: 'latency', + }); + + const searchRate = + ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_UNKNOWN_RATE; + const bulkRate = 0; + + expect(avg(response.body.currentTimeseries)).to.eql( + roundNumber( + ((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) / + (searchRate + bulkRate)) * + 1000 + ) + ); + }); + + it('returns the correct throughput', async () => { + const response = await callApi({ + backendName: 'redis', + searchServiceDestinationMetrics: false, + spanName: 'SET', + metric: 'throughput', + }); + + expect(avg(response.body.currentTimeseries)).to.eql(REDIS_SET_RATE); + }); + + it('returns the correct failure rate', async () => { + const response = await callApi({ + backendName: 'elasticsearch', + searchServiceDestinationMetrics: false, + spanName: '/_bulk', + metric: 'error_rate', + }); + + expect(avg(response.body.currentTimeseries)).to.eql(null); + }); + }); + + after(() => synthtraceEsClient.clean()); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/dependencies/generate_operation_data.ts b/x-pack/test/apm_api_integration/tests/dependencies/generate_operation_data.ts new file mode 100644 index 0000000000000..d69ff4a290430 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/dependencies/generate_operation_data.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { apm, timerange } from '@elastic/apm-synthtrace'; +import { ApmSynthtraceEsClient } from '@elastic/apm-synthtrace'; + +export const generateOperationDataConfig = { + ES_SEARCH_DURATION: 100, + ES_SEARCH_UNKNOWN_RATE: 5, + ES_BULK_RATE: 20, + ES_SEARCH_SUCCESS_RATE: 4, + ES_SEARCH_FAILURE_RATE: 1, + ES_BULK_DURATION: 1000, + REDIS_SET_RATE: 10, + REDIS_SET_DURATION: 10, +}; + +export async function generateOperationData({ + start, + end, + synthtraceEsClient, +}: { + start: number; + end: number; + synthtraceEsClient: ApmSynthtraceEsClient; +}) { + const synthGoInstance = apm.service('synth-go', 'production', 'go').instance('instance-a'); + const synthJavaInstance = apm.service('synth-java', 'development', 'java').instance('instance-a'); + + const interval = timerange(start, end).interval('1m'); + + return await synthtraceEsClient.index([ + interval + .rate(generateOperationDataConfig.ES_SEARCH_UNKNOWN_RATE) + .generator((timestamp) => + synthGoInstance + .span('/_search', 'db', 'elasticsearch') + .destination('elasticsearch') + .timestamp(timestamp) + .duration(generateOperationDataConfig.ES_SEARCH_DURATION) + ), + interval + .rate(generateOperationDataConfig.ES_SEARCH_SUCCESS_RATE) + .generator((timestamp) => + synthGoInstance + .span('/_search', 'db', 'elasticsearch') + .destination('elasticsearch') + .timestamp(timestamp) + .success() + .duration(generateOperationDataConfig.ES_SEARCH_DURATION) + ), + interval + .rate(generateOperationDataConfig.ES_SEARCH_FAILURE_RATE) + .generator((timestamp) => + synthGoInstance + .span('/_search', 'db', 'elasticsearch') + .destination('elasticsearch') + .timestamp(timestamp) + .failure() + .duration(generateOperationDataConfig.ES_SEARCH_DURATION) + ), + interval + .rate(generateOperationDataConfig.ES_BULK_RATE) + .generator((timestamp) => + synthJavaInstance + .span('/_bulk', 'db', 'elasticsearch') + .destination('elasticsearch') + .timestamp(timestamp) + .duration(generateOperationDataConfig.ES_BULK_DURATION) + ), + interval + .rate(generateOperationDataConfig.REDIS_SET_RATE) + .generator((timestamp) => + synthJavaInstance + .span('SET', 'db', 'redis') + .destination('redis') + .timestamp(timestamp) + .duration(generateOperationDataConfig.REDIS_SET_DURATION) + ), + ]); +} diff --git a/x-pack/test/apm_api_integration/tests/dependencies/top_operations.spec.ts b/x-pack/test/apm_api_integration/tests/dependencies/top_operations.spec.ts index e7b73a40ede83..638d9740cf17d 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/top_operations.spec.ts +++ b/x-pack/test/apm_api_integration/tests/dependencies/top_operations.spec.ts @@ -6,14 +6,25 @@ */ import expect from '@kbn/expect'; import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { apm, timerange } from '@elastic/apm-synthtrace'; import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values'; import { ValuesType } from 'utility-types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { roundNumber } from '../../utils'; +import { generateOperationData, generateOperationDataConfig } from './generate_operation_data'; type TopOperations = APIReturnType<'GET /internal/apm/backends/operations'>['operations']; +const { + ES_BULK_DURATION, + ES_BULK_RATE, + ES_SEARCH_DURATION, + ES_SEARCH_FAILURE_RATE, + ES_SEARCH_SUCCESS_RATE, + ES_SEARCH_UNKNOWN_RATE, + REDIS_SET_DURATION, + REDIS_SET_RATE, +} = generateOperationDataConfig; + export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); @@ -47,76 +58,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { .then(({ body }) => body.operations); } - const ES_SEARCH_DURATION = 100; - const ES_SEARCH_UNKNOWN_RATE = 5; - const ES_SEARCH_SUCCESS_RATE = 4; - const ES_SEARCH_FAILURE_RATE = 1; - - const ES_BULK_RATE = 20; - const ES_BULK_DURATION = 1000; - - const REDIS_SET_RATE = 10; - const REDIS_SET_DURATION = 10; - - async function generateData() { - const synthGoInstance = apm.service('synth-go', 'production', 'go').instance('instance-a'); - const synthJavaInstance = apm - .service('synth-java', 'development', 'java') - .instance('instance-a'); - - const interval = timerange(start, end).interval('1m'); - - return await synthtraceEsClient.index([ - interval - .rate(ES_SEARCH_UNKNOWN_RATE) - .generator((timestamp) => - synthGoInstance - .span('/_search', 'db', 'elasticsearch') - .destination('elasticsearch') - .timestamp(timestamp) - .duration(ES_SEARCH_DURATION) - ), - interval - .rate(ES_SEARCH_SUCCESS_RATE) - .generator((timestamp) => - synthGoInstance - .span('/_search', 'db', 'elasticsearch') - .destination('elasticsearch') - .timestamp(timestamp) - .success() - .duration(ES_SEARCH_DURATION) - ), - interval - .rate(ES_SEARCH_FAILURE_RATE) - .generator((timestamp) => - synthGoInstance - .span('/_search', 'db', 'elasticsearch') - .destination('elasticsearch') - .timestamp(timestamp) - .failure() - .duration(ES_SEARCH_DURATION) - ), - interval - .rate(ES_BULK_RATE) - .generator((timestamp) => - synthJavaInstance - .span('/_bulk', 'db', 'elasticsearch') - .destination('elasticsearch') - .timestamp(timestamp) - .duration(ES_BULK_DURATION) - ), - interval - .rate(REDIS_SET_RATE) - .generator((timestamp) => - synthJavaInstance - .span('SET', 'db', 'redis') - .destination('redis') - .timestamp(timestamp) - .duration(REDIS_SET_DURATION) - ), - ]); - } - registry.when('Top operations when data is not loaded', { config: 'basic', archives: [] }, () => { it('handles empty state', async () => { const operations = await callApi({ backendName: 'elasticsearch' }); @@ -128,7 +69,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { 'Top operations when data is generated', { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, () => { - before(() => generateData()); + before(() => + generateOperationData({ + synthtraceEsClient, + start, + end, + }) + ); after(() => synthtraceEsClient.clean()); diff --git a/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts b/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts index ee7c27aa6ddd4..dd582238d4dd4 100644 --- a/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts +++ b/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts @@ -43,6 +43,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { query: { ...commonQuery, backendName: overrides?.backendName || 'elasticsearch', + spanName: '', + searchServiceDestinationMetrics: false, kuery: '', }, }, From 23f091b183d6b94631b9d048090379b79d367742 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Mon, 13 Jun 2022 10:48:57 +0200 Subject: [PATCH 16/62] [Discover] Update toast copy (#134178) --- .../discover/public/utils/copy_value_to_clipboard.test.tsx | 6 +++--- .../discover/public/utils/copy_value_to_clipboard.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/discover/public/utils/copy_value_to_clipboard.test.tsx b/src/plugins/discover/public/utils/copy_value_to_clipboard.test.tsx index 004332f9a20cc..3f3beed866eca 100644 --- a/src/plugins/discover/public/utils/copy_value_to_clipboard.test.tsx +++ b/src/plugins/discover/public/utils/copy_value_to_clipboard.test.tsx @@ -127,7 +127,7 @@ describe('copyValueToClipboard', () => { '"bool_enabled"\nfalse\ntrue' ); expect(discoverServiceMock.toastNotifications.addInfo).toHaveBeenCalledWith({ - title: 'Copied values of "bool_enabled" column to clipboard', + title: 'Values of "bool_enabled" column copied to clipboard', }); }); @@ -142,8 +142,8 @@ describe('copyValueToClipboard', () => { expect(result).toBe('"scripted_string"\n"hi there"\n"\'=1+2"";=1+2"'); expect(discoverServiceMock.toastNotifications.addWarning).toHaveBeenCalledWith({ - title: 'Copied values of "scripted_string" column to clipboard', - text: 'It may contain formulas whose values have been escaped.', + title: 'Values of "scripted_string" column copied to clipboard', + text: 'Values may contain formulas that are escaped.', }); }); }); diff --git a/src/plugins/discover/public/utils/copy_value_to_clipboard.ts b/src/plugins/discover/public/utils/copy_value_to_clipboard.ts index eedaa676a4c56..4a857ebb9e04c 100644 --- a/src/plugins/discover/public/utils/copy_value_to_clipboard.ts +++ b/src/plugins/discover/public/utils/copy_value_to_clipboard.ts @@ -15,7 +15,7 @@ import { convertNameToString } from './convert_value_to_string'; const WARNING_FOR_FORMULAS = i18n.translate( 'discover.grid.copyEscapedValueWithFormulasToClipboardWarningText', { - defaultMessage: 'It may contain formulas whose values have been escaped.', + defaultMessage: 'Values may contain formulas that are escaped.', } ); const COPY_FAILED_ERROR_MESSAGE = i18n.translate('discover.grid.copyFailedErrorText', { @@ -107,7 +107,7 @@ export const copyColumnValuesToClipboard = async ({ } const toastTitle = i18n.translate('discover.grid.copyColumnValuesToClipboard.toastTitle', { - defaultMessage: 'Copied values of "{column}" column to clipboard', + defaultMessage: 'Values of "{column}" column copied to clipboard', values: { column: columnId }, }); From 8e81a6e569aa92f1b5dd1aa7aec9d99dd5a9f963 Mon Sep 17 00:00:00 2001 From: "Lucas F. da Costa" Date: Mon, 13 Jun 2022 10:43:00 +0100 Subject: [PATCH 17/62] [Uptime] fix sending flag to indicate edits when updating service monitors (#133608) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../routes/monitor_cruds/edit_monitor.test.ts | 81 +++++++++++++++++++ .../routes/monitor_cruds/edit_monitor.ts | 17 ++-- 2 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.test.ts diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.test.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.test.ts new file mode 100644 index 0000000000000..35b6ba08e6965 --- /dev/null +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggerMock } from '@kbn/logging-mocks'; +import { syncEditedMonitor } from './edit_monitor'; +import { SavedObjectsUpdateResponse, SavedObject } from '@kbn/core/server'; +import { EncryptedSyntheticsMonitor, SyntheticsMonitor } from '../../../common/runtime_types'; +import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters'; +import { SyntheticsService } from '../../synthetics_service/synthetics_service'; + +jest.mock('../telemetry/monitor_upgrade_sender', () => ({ + sendTelemetryEvents: jest.fn(), + formatTelemetryUpdateEvent: jest.fn(), +})); + +describe('syncEditedMonitor', () => { + const logger = loggerMock.create(); + + const serverMock: UptimeServerSetup = { + uptimeEsClient: { search: jest.fn() }, + kibanaVersion: null, + authSavedObjectsClient: { bulkUpdate: jest.fn() }, + logger, + } as unknown as UptimeServerSetup; + + const syntheticsService = new SyntheticsService(logger, serverMock, { + username: 'dev', + password: '12345', + }); + + const fakePush = jest.fn(); + + jest.spyOn(syntheticsService, 'pushConfigs').mockImplementationOnce(fakePush); + + serverMock.syntheticsService = syntheticsService; + + const editedMonitor = { + type: 'http', + enabled: true, + schedule: { + number: '3', + unit: 'm', + }, + name: 'my mon', + locations: [], + urls: 'http://google.com', + max_redirects: '0', + password: '', + proxy_url: '', + id: '7af7e2f0-d5dc-11ec-87ac-bdfdb894c53d', + fields: { config_id: '7af7e2f0-d5dc-11ec-87ac-bdfdb894c53d' }, + fields_under_root: true, + } as unknown as SyntheticsMonitor; + + const previousMonitor = { id: 'saved-obj-id' } as SavedObject; + const editedMonitorSavedObject = { + id: 'saved-obj-id', + } as SavedObjectsUpdateResponse; + + it('includes the isEdit flag', () => { + syncEditedMonitor({ + editedMonitor, + editedMonitorSavedObject, + previousMonitor, + server: serverMock, + }); + + expect(fakePush).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + id: 'saved-obj-id', + }), + ]), + true + ); + }); +}); diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts index ce99f60d66aaa..d42a2b33b291e 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts @@ -127,13 +127,16 @@ export const syncEditedMonitor = async ({ previousMonitor: SavedObject; server: UptimeServerSetup; }) => { - const errors = await server.syntheticsService.pushConfigs([ - formatHeartbeatRequest({ - monitor: editedMonitor, - monitorId: editedMonitorSavedObject.id, - customHeartbeatId: (editedMonitor as MonitorFields)[ConfigKey.CUSTOM_HEARTBEAT_ID], - }), - ]); + const errors = await server.syntheticsService.pushConfigs( + [ + formatHeartbeatRequest({ + monitor: editedMonitor, + monitorId: editedMonitorSavedObject.id, + customHeartbeatId: (editedMonitor as MonitorFields)[ConfigKey.CUSTOM_HEARTBEAT_ID], + }), + ], + true + ); sendTelemetryEvents( server.logger, From 027dad3d3c9fdb69a3384850f1dac9672aeae7fb Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Mon, 13 Jun 2022 12:16:23 +0200 Subject: [PATCH 18/62] [ML] Functional tests - skip lang_ident_model_1 in saved object cleanup (#134028) This PR skips the cleanup of the lang_ident_model_1 saved object for functional ML tests. --- x-pack/test/functional/services/ml/test_resources.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/test/functional/services/ml/test_resources.ts b/x-pack/test/functional/services/ml/test_resources.ts index 3115eca61b443..1cbe8670cd258 100644 --- a/x-pack/test/functional/services/ml/test_resources.ts +++ b/x-pack/test/functional/services/ml/test_resources.ts @@ -483,6 +483,10 @@ export function MachineLearningTestResourcesProvider( SavedObjectType.ML_TRAINED_MODEL_SAVED_OBJECT_TYPE ); for (const id of savedObjectIds) { + if (id === 'lang_ident_model_1') { + log.debug('> Skipping internal lang_ident_model_1'); + continue; + } await this.deleteSavedObjectById( id, SavedObjectType.ML_TRAINED_MODEL_SAVED_OBJECT_TYPE, From 34fa750da5d81cf71592a77aad0cc3bb131158f5 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Mon, 13 Jun 2022 06:33:22 -0400 Subject: [PATCH 19/62] [UX Dashboard] Move core web vitals query from APM routes to UX plugin (#133974) * Migrate service list query out of APM. * Rename non-snakecase files. * Migrate service list query out of APM. * Rename non-snakecase files. * Move core web vitals query out of APM routes. * Delete obsolete snapshot. * Refactor o11y overview registration to not rely on deleted APM route. * Fix some types. * Delete obsolete API test. * Delete obsolete APM API tests. * Delete CWV test from APM API suite. * Add test journey for core web vitals. Co-authored-by: Shahzad --- .../__snapshots__/queries.test.ts.snap | 118 --- .../server/routes/rum_client/queries.test.ts | 15 - .../apm/server/routes/rum_client/route.ts | 37 - .../ux/e2e/journeys/core_web_vitals.ts | 56 ++ x-pack/plugins/ux/e2e/journeys/index.ts | 1 + .../app/rum_dashboard/ux_metrics/index.tsx | 24 +- .../app/rum_dashboard/ux_overview_fetchers.ts | 82 +- .../public/hooks/use_core_web_vitals_query.ts | 44 + x-pack/plugins/ux/public/plugin.ts | 8 +- .../core_web_vitals_query.test.ts.snap | 114 +++ .../data/core_web_vitals_query.test.ts | 24 + .../services/data/core_web_vitals_query.ts} | 143 +-- .../ux/public/services/data/projections.ts | 12 +- .../services/data/service_name_query.ts | 2 +- .../__snapshots__/page_load_dist.spec.snap | 832 ------------------ .../tests/csm/page_load_dist.spec.ts | 73 -- .../tests/csm/web_core_vitals.spec.ts | 78 -- 17 files changed, 402 insertions(+), 1261 deletions(-) create mode 100644 x-pack/plugins/ux/e2e/journeys/core_web_vitals.ts create mode 100644 x-pack/plugins/ux/public/hooks/use_core_web_vitals_query.ts create mode 100644 x-pack/plugins/ux/public/services/data/__snapshots__/core_web_vitals_query.test.ts.snap create mode 100644 x-pack/plugins/ux/public/services/data/core_web_vitals_query.test.ts rename x-pack/plugins/{apm/server/routes/rum_client/get_web_core_vitals.ts => ux/public/services/data/core_web_vitals_query.ts} (64%) delete mode 100644 x-pack/test/apm_api_integration/tests/csm/__snapshots__/page_load_dist.spec.snap delete mode 100644 x-pack/test/apm_api_integration/tests/csm/page_load_dist.spec.ts delete mode 100644 x-pack/test/apm_api_integration/tests/csm/web_core_vitals.spec.ts diff --git a/x-pack/plugins/apm/server/routes/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/routes/rum_client/__snapshots__/queries.test.ts.snap index bad786579a605..5931582340943 100644 --- a/x-pack/plugins/apm/server/routes/rum_client/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/routes/rum_client/__snapshots__/queries.test.ts.snap @@ -465,121 +465,3 @@ Object { }, } `; - -exports[`rum client dashboard queries fetches rum core vitals 1`] = ` -Object { - "apm": Object { - "events": Array [ - "transaction", - ], - }, - "body": Object { - "aggs": Object { - "cls": Object { - "percentiles": Object { - "field": "transaction.experience.cls", - "percents": Array [ - 50, - ], - }, - }, - "clsRanks": Object { - "percentile_ranks": Object { - "field": "transaction.experience.cls", - "keyed": false, - "values": Array [ - 0.1, - 0.25, - ], - }, - }, - "coreVitalPages": Object { - "filter": Object { - "exists": Object { - "field": "transaction.experience", - }, - }, - }, - "fcp": Object { - "percentiles": Object { - "field": "transaction.marks.agent.firstContentfulPaint", - "percents": Array [ - 50, - ], - }, - }, - "fid": Object { - "percentiles": Object { - "field": "transaction.experience.fid", - "percents": Array [ - 50, - ], - }, - }, - "fidRanks": Object { - "percentile_ranks": Object { - "field": "transaction.experience.fid", - "keyed": false, - "values": Array [ - 100, - 300, - ], - }, - }, - "lcp": Object { - "percentiles": Object { - "field": "transaction.marks.agent.largestContentfulPaint", - "percents": Array [ - 50, - ], - }, - }, - "lcpRanks": Object { - "percentile_ranks": Object { - "field": "transaction.marks.agent.largestContentfulPaint", - "keyed": false, - "values": Array [ - 2500, - 4000, - ], - }, - }, - "tbt": Object { - "percentiles": Object { - "field": "transaction.experience.tbt", - "percents": Array [ - 50, - ], - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 0, - "lte": 50000, - }, - }, - }, - Object { - "term": Object { - "transaction.type": "page-load", - }, - }, - Object { - "exists": Object { - "field": "transaction.marks.navigationTiming.fetchStart", - }, - }, - ], - "must_not": Array [], - }, - }, - "size": 0, - }, -} -`; diff --git a/x-pack/plugins/apm/server/routes/rum_client/queries.test.ts b/x-pack/plugins/apm/server/routes/rum_client/queries.test.ts index 9fe54ac1f7701..43c115c75567c 100644 --- a/x-pack/plugins/apm/server/routes/rum_client/queries.test.ts +++ b/x-pack/plugins/apm/server/routes/rum_client/queries.test.ts @@ -13,8 +13,6 @@ import { getClientMetrics } from './get_client_metrics'; import { getPageViewTrends } from './get_page_view_trends'; import { getPageLoadDistribution } from './get_page_load_distribution'; import { getLongTaskMetrics } from './get_long_task_metrics'; -import { getWebCoreVitals } from './get_web_core_vitals'; -import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; describe('rum client dashboard queries', () => { let mock: SearchParamsMock; @@ -66,19 +64,6 @@ describe('rum client dashboard queries', () => { expect(mock.params).toMatchSnapshot(); }); - it('fetches rum core vitals', async () => { - mock = await inspectSearchParams( - (setup) => - getWebCoreVitals({ - setup, - start: 0, - end: 50000, - }), - { uiFilters: { environment: ENVIRONMENT_ALL.value } } - ); - expect(mock.params).toMatchSnapshot(); - }); - it('fetches long task metrics', async () => { mock = await inspectSearchParams((setup) => getLongTaskMetrics({ diff --git a/x-pack/plugins/apm/server/routes/rum_client/route.ts b/x-pack/plugins/apm/server/routes/rum_client/route.ts index c15c07b322997..612b00b932e24 100644 --- a/x-pack/plugins/apm/server/routes/rum_client/route.ts +++ b/x-pack/plugins/apm/server/routes/rum_client/route.ts @@ -14,7 +14,6 @@ import { getPageLoadDistribution } from './get_page_load_distribution'; import { getPageViewTrends } from './get_page_view_trends'; import { getPageLoadDistBreakdown } from './get_pl_dist_breakdown'; import { getVisitorBreakdown } from './get_visitor_breakdown'; -import { getWebCoreVitals } from './get_web_core_vitals'; import { hasRumData } from './has_rum_data'; import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; import { rangeRt } from '../default_api_types'; @@ -214,41 +213,6 @@ const rumVisitorsBreakdownRoute = createApmServerRoute({ }, }); -const rumWebCoreVitals = createApmServerRoute({ - endpoint: 'GET /internal/apm/ux/web-core-vitals', - params: t.type({ - query: uxQueryRt, - }), - options: { tags: ['access:apm'] }, - handler: async ( - resources - ): Promise<{ - coreVitalPages: number; - cls: number | null; - fid: number | null | undefined; - lcp: number | null | undefined; - tbt: number; - fcp: number | null | undefined; - lcpRanks: number[]; - fidRanks: number[]; - clsRanks: number[]; - }> => { - const setup = await setupUXRequest(resources); - - const { - query: { urlQuery, percentile, start, end }, - } = resources.params; - - return getWebCoreVitals({ - setup, - urlQuery, - percentile: percentile ? Number(percentile) : undefined, - start, - end, - }); - }, -}); - const rumLongTaskMetrics = createApmServerRoute({ endpoint: 'GET /internal/apm/ux/long-task-metrics', params: t.type({ @@ -338,7 +302,6 @@ export const rumRouteRepository = { ...rumPageLoadDistBreakdownRoute, ...rumPageViewsTrendRoute, ...rumVisitorsBreakdownRoute, - ...rumWebCoreVitals, ...rumLongTaskMetrics, ...rumHasDataRoute, }; diff --git a/x-pack/plugins/ux/e2e/journeys/core_web_vitals.ts b/x-pack/plugins/ux/e2e/journeys/core_web_vitals.ts new file mode 100644 index 0000000000000..2a9d83b83d66a --- /dev/null +++ b/x-pack/plugins/ux/e2e/journeys/core_web_vitals.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { journey, step, expect, before } from '@elastic/synthetics'; +import { UXDashboardDatePicker } from '../page_objects/date_picker'; +import { loginToKibana, waitForLoadingToFinish } from './utils'; + +journey('Core Web Vitals', async ({ page, params }) => { + before(async () => { + await waitForLoadingToFinish({ page }); + }); + + const queryParams = { + percentile: '50', + rangeFrom: '2020-05-18T11:51:00.000Z', + rangeTo: '2021-10-30T06:37:15.536Z', + }; + const queryString = new URLSearchParams(queryParams).toString(); + + const baseUrl = `${params.kibanaUrl}/app/ux`; + + step('Go to UX Dashboard', async () => { + await page.goto(`${baseUrl}?${queryString}`, { + waitUntil: 'networkidle', + }); + await loginToKibana({ + page, + user: { username: 'viewer_user', password: 'changeme' }, + }); + }); + + step('Set date range', async () => { + const datePickerPage = new UXDashboardDatePicker(page); + await datePickerPage.setDefaultE2eRange(); + }); + + step('Check Core Web Vitals', async () => { + expect(await page.$('text=Largest contentful paint')); + expect(await page.$('text=First input delay')); + expect(await page.$('text=Cumulative layout shift')); + expect( + await page.innerText('text=1.93 s', { + strict: true, + timeout: 29000, + }) + ).toEqual('1.93 s'); + expect(await page.innerText('text=5 ms', { strict: true })).toEqual('5 ms'); + expect(await page.innerText('text=0.003', { strict: true })).toEqual( + '0.003' + ); + }); +}); diff --git a/x-pack/plugins/ux/e2e/journeys/index.ts b/x-pack/plugins/ux/e2e/journeys/index.ts index 5800960d57be1..7440afe377ac3 100644 --- a/x-pack/plugins/ux/e2e/journeys/index.ts +++ b/x-pack/plugins/ux/e2e/journeys/index.ts @@ -5,5 +5,6 @@ * 2.0. */ +export * from './core_web_vitals'; export * from './url_ux_query.journey'; export * from './ux_js_errors.journey'; diff --git a/x-pack/plugins/ux/public/components/app/rum_dashboard/ux_metrics/index.tsx b/x-pack/plugins/ux/public/components/app/rum_dashboard/ux_metrics/index.tsx index a5689a7701027..9d56416ab9f1f 100644 --- a/x-pack/plugins/ux/public/components/app/rum_dashboard/ux_metrics/index.tsx +++ b/x-pack/plugins/ux/public/components/app/rum_dashboard/ux_metrics/index.tsx @@ -17,11 +17,11 @@ import { import { getCoreVitalsComponent } from '@kbn/observability-plugin/public'; import { I18LABELS } from '../translations'; import { KeyUXMetrics } from './key_ux_metrics'; -import { useFetcher } from '../../../../hooks/use_fetcher'; import { useUxQuery } from '../hooks/use_ux_query'; import { CsmSharedContext } from '../csm_shared_context'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; import { getPercentileLabel } from './translations'; +import { useCoreWebVitalsQuery } from '../../../../hooks/use_core_web_vitals_query'; export function UXMetrics() { const { @@ -30,19 +30,9 @@ export function UXMetrics() { const uxQuery = useUxQuery(); - const { data, status } = useFetcher( - (callApmApi) => { - if (uxQuery) { - return callApmApi('GET /internal/apm/ux/web-core-vitals', { - params: { - query: uxQuery, - }, - }); - } - return Promise.resolve(null); - }, - [uxQuery] - ); + const { data, loading: loadingResponse } = useCoreWebVitalsQuery(uxQuery); + + const loading = loadingResponse ?? true; const { sharedData: { totalPageViews }, @@ -53,11 +43,11 @@ export function UXMetrics() { getCoreVitalsComponent({ data, totalPageViews, - loading: status !== 'success', + loading, displayTrafficMetric: true, }), // eslint-disable-next-line react-hooks/exhaustive-deps - [status] + [loading] ); return ( @@ -70,7 +60,7 @@ export function UXMetrics() { - + diff --git a/x-pack/plugins/ux/public/components/app/rum_dashboard/ux_overview_fetchers.ts b/x-pack/plugins/ux/public/components/app/rum_dashboard/ux_overview_fetchers.ts index 5d749b1320196..3f774375e39ea 100644 --- a/x-pack/plugins/ux/public/components/app/rum_dashboard/ux_overview_fetchers.ts +++ b/x-pack/plugins/ux/public/components/app/rum_dashboard/ux_overview_fetchers.ts @@ -5,35 +5,91 @@ * 2.0. */ +import { ESSearchResponse } from '@kbn/core/types/elasticsearch'; +import { + DataPublicPluginStart, + isCompleteResponse, +} from '@kbn/data-plugin/public'; import { FetchDataParams, HasDataParams, UxFetchDataResponse, UXHasDataResponse, + UXMetrics, } from '@kbn/observability-plugin/public'; +import { + coreWebVitalsQuery, + transformCoreWebVitalsResponse, + DEFAULT_RANKS, +} from '../../../services/data/core_web_vitals_query'; import { callApmApi } from '../../../services/rest/create_call_apm_api'; export { createCallApmApi } from '../../../services/rest/create_call_apm_api'; -export const fetchUxOverviewDate = async ({ +type FetchUxOverviewDateParams = FetchDataParams & { + dataStartPlugin: DataPublicPluginStart; +}; + +async function getCoreWebVitalsResponse({ absoluteTime, - relativeTime, serviceName, -}: FetchDataParams): Promise => { - const data = await callApmApi('GET /internal/apm/ux/web-core-vitals', { + dataStartPlugin, +}: FetchUxOverviewDateParams) { + const dataView = await callApmApi('GET /internal/apm/data_view/dynamic', { signal: null, - params: { - query: { - start: new Date(absoluteTime.start).toISOString(), - end: new Date(absoluteTime.end).toISOString(), - uiFilters: `{"serviceName":["${serviceName}"]}`, - }, - }, }); + return new Promise< + ESSearchResponse<{}, ReturnType> + >((resolve) => { + const search$ = dataStartPlugin.search + .search( + { + params: { + index: dataView.dynamicDataView?.title, + ...coreWebVitalsQuery( + absoluteTime.start, + absoluteTime.end, + undefined, + { + serviceName: serviceName ? [serviceName] : undefined, + } + ), + }, + }, + {} + ) + .subscribe({ + next: (result) => { + if (isCompleteResponse(result)) { + resolve(result.rawResponse as any); + search$.unsubscribe(); + } + }, + }); + }); +} + +const CORE_WEB_VITALS_DEFAULTS: UXMetrics = { + coreVitalPages: 0, + cls: 0, + fid: 0, + lcp: 0, + tbt: 0, + fcp: 0, + lcpRanks: DEFAULT_RANKS, + fidRanks: DEFAULT_RANKS, + clsRanks: DEFAULT_RANKS, +}; +export const fetchUxOverviewDate = async ( + params: FetchUxOverviewDateParams +): Promise => { + const coreWebVitalsResponse = await getCoreWebVitalsResponse(params); return { - coreWebVitals: data, - appLink: `/app/ux?rangeFrom=${relativeTime.start}&rangeTo=${relativeTime.end}`, + coreWebVitals: + transformCoreWebVitalsResponse(coreWebVitalsResponse) ?? + CORE_WEB_VITALS_DEFAULTS, + appLink: `/app/ux?rangeFrom=${params.relativeTime.start}&rangeTo=${params.relativeTime.end}`, }; }; diff --git a/x-pack/plugins/ux/public/hooks/use_core_web_vitals_query.ts b/x-pack/plugins/ux/public/hooks/use_core_web_vitals_query.ts new file mode 100644 index 0000000000000..8af6060d498e6 --- /dev/null +++ b/x-pack/plugins/ux/public/hooks/use_core_web_vitals_query.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEsSearch } from '@kbn/observability-plugin/public'; +import { useMemo } from 'react'; +import { useDataView } from '../components/app/rum_dashboard/local_uifilters/use_data_view'; +import { callDateMath } from '../services/data/call_date_math'; +import { + coreWebVitalsQuery, + transformCoreWebVitalsResponse, + PERCENTILE_DEFAULT, +} from '../services/data/core_web_vitals_query'; +import { useUxQuery } from '../components/app/rum_dashboard/hooks/use_ux_query'; + +export function useCoreWebVitalsQuery(uxQuery: ReturnType) { + const { dataViewTitle } = useDataView(); + const { data: esQueryResponse, loading } = useEsSearch( + { + index: uxQuery ? dataViewTitle : undefined, + ...coreWebVitalsQuery( + callDateMath(uxQuery?.start), + callDateMath(uxQuery?.end), + uxQuery?.urlQuery, + uxQuery?.uiFilters ? JSON.parse(uxQuery.uiFilters) : {}, + uxQuery?.percentile ? Number(uxQuery.percentile) : undefined + ), + }, + [uxQuery, dataViewTitle], + { name: 'UxCoreWebVitals' } + ); + const data = useMemo( + () => + transformCoreWebVitalsResponse( + esQueryResponse, + uxQuery?.percentile ? Number(uxQuery?.percentile) : PERCENTILE_DEFAULT + ), + [esQueryResponse, uxQuery?.percentile] + ); + return { data, loading }; +} diff --git a/x-pack/plugins/ux/public/plugin.ts b/x-pack/plugins/ux/public/plugin.ts index fe98a490b5b80..aff2481a0f0b0 100644 --- a/x-pack/plugins/ux/public/plugin.ts +++ b/x-pack/plugins/ux/public/plugin.ts @@ -79,8 +79,14 @@ export class UxPlugin implements Plugin { return await dataHelper.hasRumData(params!); }, fetchData: async (params: FetchDataParams) => { + const [_, startPlugins] = await core.getStartServices(); + + const { data: dataStartPlugin } = startPlugins as ApmPluginStartDeps; const dataHelper = await getUxDataHelper(); - return await dataHelper.fetchUxOverviewDate(params); + return await dataHelper.fetchUxOverviewDate({ + ...params, + dataStartPlugin, + }); }, }); } diff --git a/x-pack/plugins/ux/public/services/data/__snapshots__/core_web_vitals_query.test.ts.snap b/x-pack/plugins/ux/public/services/data/__snapshots__/core_web_vitals_query.test.ts.snap new file mode 100644 index 0000000000000..8810a3e97aca8 --- /dev/null +++ b/x-pack/plugins/ux/public/services/data/__snapshots__/core_web_vitals_query.test.ts.snap @@ -0,0 +1,114 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`core web vitals query fetches rum core vitals 1`] = ` +Object { + "body": Object { + "aggs": Object { + "cls": Object { + "percentiles": Object { + "field": "transaction.experience.cls", + "percents": Array [ + 50, + ], + }, + }, + "clsRanks": Object { + "percentile_ranks": Object { + "field": "transaction.experience.cls", + "keyed": false, + "values": Array [ + 0.1, + 0.25, + ], + }, + }, + "coreVitalPages": Object { + "filter": Object { + "exists": Object { + "field": "transaction.experience", + }, + }, + }, + "fcp": Object { + "percentiles": Object { + "field": "transaction.marks.agent.firstContentfulPaint", + "percents": Array [ + 50, + ], + }, + }, + "fid": Object { + "percentiles": Object { + "field": "transaction.experience.fid", + "percents": Array [ + 50, + ], + }, + }, + "fidRanks": Object { + "percentile_ranks": Object { + "field": "transaction.experience.fid", + "keyed": false, + "values": Array [ + 100, + 300, + ], + }, + }, + "lcp": Object { + "percentiles": Object { + "field": "transaction.marks.agent.largestContentfulPaint", + "percents": Array [ + 50, + ], + }, + }, + "lcpRanks": Object { + "percentile_ranks": Object { + "field": "transaction.marks.agent.largestContentfulPaint", + "keyed": false, + "values": Array [ + 2500, + 4000, + ], + }, + }, + "tbt": Object { + "percentiles": Object { + "field": "transaction.experience.tbt", + "percents": Array [ + 50, + ], + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 0, + "lte": 5000, + }, + }, + }, + Object { + "term": Object { + "transaction.type": "page-load", + }, + }, + Object { + "exists": Object { + "field": "transaction.marks.navigationTiming.fetchStart", + }, + }, + ], + "must_not": Array [], + }, + }, + "size": 0, + }, +} +`; diff --git a/x-pack/plugins/ux/public/services/data/core_web_vitals_query.test.ts b/x-pack/plugins/ux/public/services/data/core_web_vitals_query.test.ts new file mode 100644 index 0000000000000..3c3e0f70a4ec0 --- /dev/null +++ b/x-pack/plugins/ux/public/services/data/core_web_vitals_query.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { coreWebVitalsQuery } from './core_web_vitals_query'; + +describe('core web vitals query', () => { + it('fetches rum core vitals', async () => { + expect( + coreWebVitalsQuery( + 0, + 5000, + '', + { + environment: 'ENVIRONMENT_ALL', + }, + 50 + ) + ).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/apm/server/routes/rum_client/get_web_core_vitals.ts b/x-pack/plugins/ux/public/services/data/core_web_vitals_query.ts similarity index 64% rename from x-pack/plugins/apm/server/routes/rum_client/get_web_core_vitals.ts rename to x-pack/plugins/ux/public/services/data/core_web_vitals_query.ts index 6a3b3a1c7b1c8..506727b315b3e 100644 --- a/x-pack/plugins/apm/server/routes/rum_client/get_web_core_vitals.ts +++ b/x-pack/plugins/ux/public/services/data/core_web_vitals_query.ts @@ -5,37 +5,89 @@ * 2.0. */ -import { getRumPageLoadTransactionsProjection } from '../../projections/rum_page_load_transactions'; -import { mergeProjection } from '../../projections/util/merge_projection'; -import { SetupUX } from './route'; +import { ESSearchResponse } from '@kbn/core/types/elasticsearch'; +import { UXMetrics } from '@kbn/observability-plugin/public'; import { - CLS_FIELD, + TBT_FIELD, FCP_FIELD, + CLS_FIELD, FID_FIELD, LCP_FIELD, - TBT_FIELD, } from '../../../common/elasticsearch_fieldnames'; +import { SetupUX, UxUIFilters } from '../../../typings/ui_filters'; +import { mergeProjection } from '../../../common/utils/merge_projection'; +import { getRumPageLoadTransactionsProjection } from './projections'; + +export const DEFAULT_RANKS = [100, 0, 0]; + +const getRanksPercentages = (ranks?: Record) => { + if (!Array.isArray(ranks)) return null; + const ranksVal = ranks?.map(({ value }) => value?.toFixed(0) ?? 0) ?? []; + return [ + Number(ranksVal?.[0]), + Number(ranksVal?.[1]) - Number(ranksVal?.[0]), + 100 - Number(ranksVal?.[1]), + ]; +}; + +export function transformCoreWebVitalsResponse( + response?: ESSearchResponse>, + percentile = PERCENTILE_DEFAULT +): UXMetrics | undefined { + if (!response) return response; + const { + lcp, + cls, + fid, + tbt, + fcp, + lcpRanks, + fidRanks, + clsRanks, + coreVitalPages, + } = response.aggregations ?? {}; + + const pkey = percentile.toFixed(1); + + return { + coreVitalPages: coreVitalPages?.doc_count ?? 0, + /* Because cls is required in the type UXMetrics, and defined as number | null, + * we need to default to null in the case where cls is undefined in order to satisfy the UXMetrics type */ + cls: cls?.values[pkey] ?? null, + fid: fid?.values[pkey], + lcp: lcp?.values[pkey], + tbt: tbt?.values[pkey] ?? 0, + fcp: fcp?.values[pkey], + + lcpRanks: lcp?.values[pkey] + ? getRanksPercentages(lcpRanks?.values) ?? DEFAULT_RANKS + : DEFAULT_RANKS, + fidRanks: fid?.values[pkey] + ? getRanksPercentages(fidRanks?.values) ?? DEFAULT_RANKS + : DEFAULT_RANKS, + clsRanks: cls?.values[pkey] + ? getRanksPercentages(clsRanks?.values) ?? DEFAULT_RANKS + : DEFAULT_RANKS, + }; +} + +export const PERCENTILE_DEFAULT = 50; + +export function coreWebVitalsQuery( + start: number, + end: number, + urlQuery?: string, + uiFilters?: UxUIFilters, + percentile = PERCENTILE_DEFAULT +) { + const setup: SetupUX = { uiFilters: uiFilters ? uiFilters : {} }; -export async function getWebCoreVitals({ - setup, - urlQuery, - percentile = 50, - start, - end, -}: { - setup: SetupUX; - urlQuery?: string; - percentile?: number; - start: number; - end: number; -}) { const projection = getRumPageLoadTransactionsProjection({ setup, urlQuery, start, end, }); - const params = mergeProjection(projection, { body: { size: 0, @@ -106,55 +158,6 @@ export async function getWebCoreVitals({ }, }, }); - - const { apmEventClient } = setup; - - const response = await apmEventClient.search('get_web_core_vitals', params); - const { - lcp, - cls, - fid, - tbt, - fcp, - lcpRanks, - fidRanks, - clsRanks, - coreVitalPages, - } = response.aggregations ?? {}; - - const getRanksPercentages = ( - ranks?: Array<{ key: number; value: number | null }> - ) => { - const ranksVal = ranks?.map(({ value }) => value?.toFixed(0) ?? 0) ?? []; - return [ - Number(ranksVal?.[0]), - Number(ranksVal?.[1]) - Number(ranksVal?.[0]), - 100 - Number(ranksVal?.[1]), - ]; - }; - - const defaultRanks = [100, 0, 0]; - - const pkey = percentile.toFixed(1); - - return { - coreVitalPages: coreVitalPages?.doc_count ?? 0, - /* Because cls is required in the type UXMetrics, and defined as number | null, - * we need to default to null in the case where cls is undefined in order to satisfy the UXMetrics type */ - cls: cls?.values[pkey] ?? null, - fid: fid?.values[pkey], - lcp: lcp?.values[pkey], - tbt: tbt?.values[pkey] ?? 0, - fcp: fcp?.values[pkey], - - lcpRanks: lcp?.values[pkey] - ? getRanksPercentages(lcpRanks?.values) - : defaultRanks, - fidRanks: fid?.values[pkey] - ? getRanksPercentages(fidRanks?.values) - : defaultRanks, - clsRanks: cls?.values[pkey] - ? getRanksPercentages(clsRanks?.values) - : defaultRanks, - }; + const { apm, ...rest } = params; + return rest; } diff --git a/x-pack/plugins/ux/public/services/data/projections.ts b/x-pack/plugins/ux/public/services/data/projections.ts index 89c3f3e8a52b9..35c5210a97e79 100644 --- a/x-pack/plugins/ux/public/services/data/projections.ts +++ b/x-pack/plugins/ux/public/services/data/projections.ts @@ -5,17 +5,17 @@ * 2.0. */ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; -import { TRANSACTION_TYPE } from '../../../common/elasticsearch_fieldnames'; +import { + AGENT_NAME, + PROCESSOR_EVENT, + SERVICE_LANGUAGE_NAME, + TRANSACTION_TYPE, +} from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; import { TRANSACTION_PAGE_LOAD } from '../../../common/transaction_types'; import { SetupUX } from '../../../typings/ui_filters'; import { getEsFilter } from './get_es_filter'; import { rangeQuery } from './range_query'; -import { - AGENT_NAME, - SERVICE_LANGUAGE_NAME, - PROCESSOR_EVENT, -} from '../../../common/elasticsearch_fieldnames'; export function getRumPageLoadTransactionsProjection({ setup, diff --git a/x-pack/plugins/ux/public/services/data/service_name_query.ts b/x-pack/plugins/ux/public/services/data/service_name_query.ts index 34dce62f0f587..1fa0809d0bd55 100644 --- a/x-pack/plugins/ux/public/services/data/service_name_query.ts +++ b/x-pack/plugins/ux/public/services/data/service_name_query.ts @@ -37,6 +37,6 @@ export function serviceNameQuery( }, }, }); - const { apm: _apm, ...rest } = params; + const { apm, ...rest } = params; return rest; } diff --git a/x-pack/test/apm_api_integration/tests/csm/__snapshots__/page_load_dist.spec.snap b/x-pack/test/apm_api_integration/tests/csm/__snapshots__/page_load_dist.spec.snap deleted file mode 100644 index 9e4a708fae304..0000000000000 --- a/x-pack/test/apm_api_integration/tests/csm/__snapshots__/page_load_dist.spec.snap +++ /dev/null @@ -1,832 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`APM API tests trial 8.0.0,rum_8.0.0 UX page load dist with data returns page load distribution 1`] = ` -Object { - "pageLoadDistribution": Object { - "maxDuration": 54.46, - "minDuration": 0, - "pageLoadDistribution": Array [ - Object { - "x": 0, - "y": 0, - }, - Object { - "x": 0.5, - "y": 0, - }, - Object { - "x": 1, - "y": 0, - }, - Object { - "x": 1.5, - "y": 0, - }, - Object { - "x": 2, - "y": 0, - }, - Object { - "x": 2.5, - "y": 0, - }, - Object { - "x": 3, - "y": 16.6666666666667, - }, - Object { - "x": 3.5, - "y": 0, - }, - Object { - "x": 4, - "y": 0, - }, - Object { - "x": 4.5, - "y": 0, - }, - Object { - "x": 5, - "y": 50, - }, - Object { - "x": 5.5, - "y": 0, - }, - Object { - "x": 6, - "y": 0, - }, - Object { - "x": 6.5, - "y": 0, - }, - Object { - "x": 7, - "y": 0, - }, - Object { - "x": 7.5, - "y": 0, - }, - Object { - "x": 8, - "y": 0, - }, - Object { - "x": 8.5, - "y": 0, - }, - Object { - "x": 9, - "y": 0, - }, - Object { - "x": 9.5, - "y": 0, - }, - Object { - "x": 10, - "y": 0, - }, - Object { - "x": 10.5, - "y": 0, - }, - Object { - "x": 11, - "y": 0, - }, - Object { - "x": 11.5, - "y": 0, - }, - Object { - "x": 12, - "y": 0, - }, - Object { - "x": 12.5, - "y": 0, - }, - Object { - "x": 13, - "y": 0, - }, - Object { - "x": 13.5, - "y": 0, - }, - Object { - "x": 14, - "y": 0, - }, - Object { - "x": 14.5, - "y": 0, - }, - Object { - "x": 15, - "y": 0, - }, - Object { - "x": 15.5, - "y": 0, - }, - Object { - "x": 16, - "y": 0, - }, - Object { - "x": 16.5, - "y": 0, - }, - Object { - "x": 17, - "y": 0, - }, - Object { - "x": 17.5, - "y": 0, - }, - Object { - "x": 18, - "y": 0, - }, - Object { - "x": 18.5, - "y": 0, - }, - Object { - "x": 19, - "y": 0, - }, - Object { - "x": 19.5, - "y": 0, - }, - Object { - "x": 20, - "y": 0, - }, - Object { - "x": 20.5, - "y": 0, - }, - Object { - "x": 21, - "y": 0, - }, - Object { - "x": 21.5, - "y": 0, - }, - Object { - "x": 22, - "y": 0, - }, - Object { - "x": 22.5, - "y": 0, - }, - Object { - "x": 23, - "y": 0, - }, - Object { - "x": 23.5, - "y": 0, - }, - Object { - "x": 24, - "y": 0, - }, - Object { - "x": 24.5, - "y": 0, - }, - Object { - "x": 25, - "y": 0, - }, - Object { - "x": 25.5, - "y": 0, - }, - Object { - "x": 26, - "y": 0, - }, - Object { - "x": 26.5, - "y": 0, - }, - Object { - "x": 27, - "y": 0, - }, - Object { - "x": 27.5, - "y": 0, - }, - Object { - "x": 28, - "y": 0, - }, - Object { - "x": 28.5, - "y": 0, - }, - Object { - "x": 29, - "y": 0, - }, - Object { - "x": 29.5, - "y": 0, - }, - Object { - "x": 30, - "y": 0, - }, - Object { - "x": 30.5, - "y": 0, - }, - Object { - "x": 31, - "y": 0, - }, - Object { - "x": 31.5, - "y": 0, - }, - Object { - "x": 32, - "y": 0, - }, - Object { - "x": 32.5, - "y": 0, - }, - Object { - "x": 33, - "y": 0, - }, - Object { - "x": 33.5, - "y": 0, - }, - Object { - "x": 34, - "y": 0, - }, - Object { - "x": 34.5, - "y": 0, - }, - Object { - "x": 35, - "y": 0, - }, - Object { - "x": 35.5, - "y": 0, - }, - Object { - "x": 36, - "y": 0, - }, - Object { - "x": 36.5, - "y": 0, - }, - Object { - "x": 37, - "y": 0, - }, - Object { - "x": 37.5, - "y": 16.6666666666667, - }, - Object { - "x": 38, - "y": 0, - }, - Object { - "x": 38.5, - "y": 0, - }, - Object { - "x": 39, - "y": 0, - }, - Object { - "x": 39.5, - "y": 0, - }, - Object { - "x": 40, - "y": 0, - }, - Object { - "x": 40.5, - "y": 0, - }, - Object { - "x": 41, - "y": 0, - }, - Object { - "x": 41.5, - "y": 0, - }, - Object { - "x": 42, - "y": 0, - }, - Object { - "x": 42.5, - "y": 0, - }, - Object { - "x": 43, - "y": 0, - }, - Object { - "x": 43.5, - "y": 0, - }, - Object { - "x": 44, - "y": 0, - }, - Object { - "x": 44.5, - "y": 0, - }, - Object { - "x": 45, - "y": 0, - }, - Object { - "x": 45.5, - "y": 0, - }, - Object { - "x": 46, - "y": 0, - }, - Object { - "x": 46.5, - "y": 0, - }, - Object { - "x": 47, - "y": 0, - }, - Object { - "x": 47.5, - "y": 0, - }, - Object { - "x": 48, - "y": 0, - }, - Object { - "x": 48.5, - "y": 0, - }, - Object { - "x": 49, - "y": 0, - }, - Object { - "x": 49.5, - "y": 0, - }, - Object { - "x": 50, - "y": 0, - }, - Object { - "x": 50.5, - "y": 0, - }, - Object { - "x": 51, - "y": 0, - }, - Object { - "x": 51.5, - "y": 0, - }, - Object { - "x": 52, - "y": 0, - }, - Object { - "x": 52.5, - "y": 0, - }, - Object { - "x": 53, - "y": 0, - }, - Object { - "x": 53.5, - "y": 0, - }, - Object { - "x": 54, - "y": 0, - }, - Object { - "x": 54.5, - "y": 16.6666666666667, - }, - ], - "percentiles": Object { - "50.0": 4.88, - "75.0": 37.09, - "90.0": 37.09, - "95.0": 54.46, - "99.0": 54.46, - }, - }, -} -`; - -exports[`APM API tests trial 8.0.0,rum_8.0.0 UX page load dist with data returns page load distribution with breakdown 1`] = ` -Object { - "pageLoadDistBreakdown": Array [ - Object { - "data": Array [ - Object { - "x": 0, - "y": 0, - }, - Object { - "x": 0.5, - "y": 0, - }, - Object { - "x": 1, - "y": 0, - }, - Object { - "x": 1.5, - "y": 0, - }, - Object { - "x": 2, - "y": 0, - }, - Object { - "x": 2.5, - "y": 0, - }, - Object { - "x": 3, - "y": 25, - }, - Object { - "x": 3.5, - "y": 0, - }, - Object { - "x": 4, - "y": 0, - }, - Object { - "x": 4.5, - "y": 0, - }, - Object { - "x": 5, - "y": 25, - }, - Object { - "x": 5.5, - "y": 0, - }, - Object { - "x": 6, - "y": 0, - }, - Object { - "x": 6.5, - "y": 0, - }, - Object { - "x": 7, - "y": 0, - }, - Object { - "x": 7.5, - "y": 0, - }, - Object { - "x": 8, - "y": 0, - }, - Object { - "x": 8.5, - "y": 0, - }, - Object { - "x": 9, - "y": 0, - }, - Object { - "x": 9.5, - "y": 0, - }, - Object { - "x": 10, - "y": 0, - }, - Object { - "x": 10.5, - "y": 0, - }, - Object { - "x": 11, - "y": 0, - }, - Object { - "x": 11.5, - "y": 0, - }, - Object { - "x": 12, - "y": 0, - }, - Object { - "x": 12.5, - "y": 0, - }, - Object { - "x": 13, - "y": 0, - }, - Object { - "x": 13.5, - "y": 0, - }, - Object { - "x": 14, - "y": 0, - }, - Object { - "x": 14.5, - "y": 0, - }, - Object { - "x": 15, - "y": 0, - }, - Object { - "x": 15.5, - "y": 0, - }, - Object { - "x": 16, - "y": 0, - }, - Object { - "x": 16.5, - "y": 0, - }, - Object { - "x": 17, - "y": 0, - }, - Object { - "x": 17.5, - "y": 0, - }, - Object { - "x": 18, - "y": 0, - }, - Object { - "x": 18.5, - "y": 0, - }, - Object { - "x": 19, - "y": 0, - }, - Object { - "x": 19.5, - "y": 0, - }, - Object { - "x": 20, - "y": 0, - }, - Object { - "x": 20.5, - "y": 0, - }, - Object { - "x": 21, - "y": 0, - }, - Object { - "x": 21.5, - "y": 0, - }, - Object { - "x": 22, - "y": 0, - }, - Object { - "x": 22.5, - "y": 0, - }, - Object { - "x": 23, - "y": 0, - }, - Object { - "x": 23.5, - "y": 0, - }, - Object { - "x": 24, - "y": 0, - }, - Object { - "x": 24.5, - "y": 0, - }, - Object { - "x": 25, - "y": 0, - }, - Object { - "x": 25.5, - "y": 0, - }, - Object { - "x": 26, - "y": 0, - }, - Object { - "x": 26.5, - "y": 0, - }, - Object { - "x": 27, - "y": 0, - }, - Object { - "x": 27.5, - "y": 0, - }, - Object { - "x": 28, - "y": 0, - }, - Object { - "x": 28.5, - "y": 0, - }, - Object { - "x": 29, - "y": 0, - }, - Object { - "x": 29.5, - "y": 0, - }, - Object { - "x": 30, - "y": 0, - }, - Object { - "x": 30.5, - "y": 0, - }, - Object { - "x": 31, - "y": 0, - }, - Object { - "x": 31.5, - "y": 0, - }, - Object { - "x": 32, - "y": 0, - }, - Object { - "x": 32.5, - "y": 0, - }, - Object { - "x": 33, - "y": 0, - }, - Object { - "x": 33.5, - "y": 0, - }, - Object { - "x": 34, - "y": 0, - }, - Object { - "x": 34.5, - "y": 0, - }, - Object { - "x": 35, - "y": 0, - }, - Object { - "x": 35.5, - "y": 0, - }, - Object { - "x": 36, - "y": 0, - }, - Object { - "x": 36.5, - "y": 0, - }, - Object { - "x": 37, - "y": 0, - }, - Object { - "x": 37.5, - "y": 25, - }, - ], - "name": "Chrome", - }, - Object { - "data": Array [ - Object { - "x": 0, - "y": 0, - }, - Object { - "x": 0.5, - "y": 0, - }, - Object { - "x": 1, - "y": 0, - }, - Object { - "x": 1.5, - "y": 0, - }, - Object { - "x": 2, - "y": 0, - }, - Object { - "x": 2.5, - "y": 0, - }, - Object { - "x": 3, - "y": 0, - }, - Object { - "x": 3.5, - "y": 0, - }, - Object { - "x": 4, - "y": 0, - }, - Object { - "x": 4.5, - "y": 0, - }, - Object { - "x": 5, - "y": 100, - }, - ], - "name": "Chrome Mobile", - }, - ], -} -`; - -exports[`APM API tests trial no data UX page load dist without data returns empty list 1`] = ` -Object { - "pageLoadDistribution": null, -} -`; - -exports[`APM API tests trial no data UX page load dist without data returns empty list with breakdowns 1`] = `Object {}`; diff --git a/x-pack/test/apm_api_integration/tests/csm/page_load_dist.spec.ts b/x-pack/test/apm_api_integration/tests/csm/page_load_dist.spec.ts deleted file mode 100644 index fd75a5cef33a2..0000000000000 --- a/x-pack/test/apm_api_integration/tests/csm/page_load_dist.spec.ts +++ /dev/null @@ -1,73 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -export default function rumServicesApiTests({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const supertest = getService('legacySupertestAsApmReadUser'); - - registry.when('UX page load dist without data', { config: 'trial', archives: [] }, () => { - it('returns empty list', async () => { - const response = await supertest.get('/internal/apm/ux/page-load-distribution').query({ - start: '2020-09-07T20:35:54.654Z', - end: '2020-09-14T20:35:54.654Z', - uiFilters: '{"serviceName":["elastic-co-rum-test"]}', - }); - - expect(response.status).to.be(200); - expectSnapshot(response.body).toMatch(); - }); - - it('returns empty list with breakdowns', async () => { - const response = await supertest - .get('/internal/apm/ux/page-load-distribution/breakdown') - .query({ - start: '2020-09-07T20:35:54.654Z', - end: '2020-09-14T20:35:54.654Z', - uiFilters: '{"serviceName":["elastic-co-rum-test"]}', - breakdown: 'Browser', - }); - - expect(response.status).to.be(200); - expectSnapshot(response.body).toMatch(); - }); - }); - - registry.when( - 'UX page load dist with data', - { config: 'trial', archives: ['8.0.0', 'rum_8.0.0'] }, - () => { - it('returns page load distribution', async () => { - const response = await supertest.get('/internal/apm/ux/page-load-distribution').query({ - start: '2020-09-07T20:35:54.654Z', - end: '2020-09-16T20:35:54.654Z', - uiFilters: '{"serviceName":["kibana-frontend-8_0_0"]}', - }); - - expect(response.status).to.be(200); - - expectSnapshot(response.body).toMatch(); - }); - it('returns page load distribution with breakdown', async () => { - const response = await supertest - .get('/internal/apm/ux/page-load-distribution/breakdown') - .query({ - start: '2020-09-07T20:35:54.654Z', - end: '2020-09-16T20:35:54.654Z', - uiFilters: '{"serviceName":["kibana-frontend-8_0_0"]}', - breakdown: 'Browser', - }); - - expect(response.status).to.be(200); - - expectSnapshot(response.body).toMatch(); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/csm/web_core_vitals.spec.ts b/x-pack/test/apm_api_integration/tests/csm/web_core_vitals.spec.ts deleted file mode 100644 index 882e9e23a4314..0000000000000 --- a/x-pack/test/apm_api_integration/tests/csm/web_core_vitals.spec.ts +++ /dev/null @@ -1,78 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -export default function rumServicesApiTests({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const supertest = getService('legacySupertestAsApmReadUser'); - - registry.when('CSM web core vitals without data', { config: 'trial', archives: [] }, () => { - it('returns empty list', async () => { - const response = await supertest.get('/internal/apm/ux/web-core-vitals').query({ - start: '2020-09-07T20:35:54.654Z', - end: '2020-09-14T20:35:54.654Z', - uiFilters: '{"serviceName":["elastic-co-rum-test"]}', - percentile: 50, - }); - - expect(response.status).to.be(200); - expect(response.body).to.eql({ - coreVitalPages: 0, - cls: null, - tbt: 0, - lcpRanks: [100, 0, 0], - fidRanks: [100, 0, 0], - clsRanks: [100, 0, 0], - }); - }); - }); - - registry.when( - 'CSM web core vitals with data', - { config: 'trial', archives: ['8.0.0', 'rum_8.0.0'] }, - () => { - it('returns web core vitals values', async () => { - const response = await supertest.get('/internal/apm/ux/web-core-vitals').query({ - start: '2020-09-07T20:35:54.654Z', - end: '2020-09-16T20:35:54.654Z', - uiFilters: '{"serviceName":["kibana-frontend-8_0_0"]}', - percentile: 50, - }); - - expect(response.status).to.be(200); - - expectSnapshot(response.body).toMatchInline(` - Object { - "cls": 0, - "clsRanks": Array [ - 100, - 0, - 0, - ], - "coreVitalPages": 6, - "fcp": 817.5, - "fid": 1352.13, - "fidRanks": Array [ - 0, - 0, - 100, - ], - "lcp": 1019, - "lcpRanks": Array [ - 100, - 0, - 0, - ], - "tbt": 0, - } - `); - }); - } - ); -} From 5df6b5fb44a29a2694fa601adccd137abb0368f0 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 13 Jun 2022 12:55:28 +0200 Subject: [PATCH 20/62] [Synthetics] Fix test now mode results display (#133121) Co-authored-by: Abdul Zahid Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../ping_timestamp/no_image_available.tsx | 36 +++++++++- .../ping_timestamp/no_image_display.test.tsx | 9 --- .../ping_timestamp/no_image_display.tsx | 26 ++------ .../ping_timestamp/ping_timestamp.test.tsx | 8 ++- .../columns/ping_timestamp/ping_timestamp.tsx | 38 +++++------ .../ping_timestamp/step_image_popover.tsx | 7 +- .../ping_timestamp/use_in_progress_image.ts | 65 +++++++++++++++++++ .../browser/browser_test_results.tsx | 3 +- .../browser/use_browser_run_once_monitors.ts | 8 ++- .../overview/empty_state/use_has_data.tsx | 13 +++- .../synthetics/check_steps/step_image.tsx | 4 +- .../check_steps/steps_list.test.tsx | 32 +++++---- .../synthetics/check_steps/steps_list.tsx | 6 +- .../pages/synthetics/synthetics_checks.tsx | 2 +- 14 files changed, 183 insertions(+), 74 deletions(-) create mode 100644 x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/use_in_progress_image.ts diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/no_image_available.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/no_image_available.tsx index 62c1392a2ed52..4fe1e8424b90b 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/no_image_available.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/no_image_available.tsx @@ -5,13 +5,18 @@ * 2.0. */ -import { EuiText } from '@elastic/eui'; +import { EuiProgress, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import React from 'react'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; +import { imageLoadingSpinnerAriaLabel } from './translations'; const BorderedText = euiStyled(EuiText)` + display: flex; + align-items: center; width: 120px; + height: 67.5px; + justify-content: center; text-align: center; border: 1px solid ${(props) => props.theme.eui.euiColorLightShade}; `; @@ -28,3 +33,32 @@ export const NoImageAvailable = () => { ); }; + +const BorderedTextLoading = euiStyled(EuiText)` + display: flex; + align-items: center; + width: 120px; + height: 65.5px; + justify-content: center; + text-align: center; + border: 1px solid ${(props) => props.theme.eui.euiColorLightShade}; +`; + +export const LoadingImageState = () => { + return ( + <> + + + + + + + + ); +}; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/no_image_display.test.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/no_image_display.test.tsx index 67990e0a7d608..0ee3f6720e982 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/no_image_display.test.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/no_image_display.test.tsx @@ -16,7 +16,6 @@ describe('NoImageDisplay', () => { defaultProps = { imageCaption:
    test caption
    , isLoading: false, - isPending: false, }; }); @@ -28,14 +27,6 @@ describe('NoImageDisplay', () => { expect(getByText('test caption')); }); - it('renders a loading spinner for pending state', () => { - defaultProps.isPending = true; - const { getByText, getByLabelText } = render(); - - expect(getByLabelText(imageLoadingSpinnerAriaLabel)); - expect(getByText('test caption')); - }); - it('renders no image available when not loading or pending', () => { const { getByText } = render(); diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/no_image_display.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/no_image_display.tsx index 6c341e7cb25ac..9cc369b2792a6 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/no_image_display.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/no_image_display.tsx @@ -5,35 +5,19 @@ * 2.0. */ -import { EuiFlexItem, EuiFlexGroup, EuiLoadingSpinner } from '@elastic/eui'; +import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import React from 'react'; -import { NoImageAvailable } from './no_image_available'; -import { imageLoadingSpinnerAriaLabel } from './translations'; +import { LoadingImageState, NoImageAvailable } from './no_image_available'; export interface NoImageDisplayProps { imageCaption: JSX.Element; - isLoading: boolean; - isPending: boolean; + isLoading?: boolean; } -export const NoImageDisplay: React.FC = ({ - imageCaption, - isLoading, - isPending, -}) => { +export const NoImageDisplay: React.FC = ({ imageCaption, isLoading }) => { return ( - - {isLoading || isPending ? ( - - ) : ( - - )} - + {isLoading ? : } {imageCaption} ); diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.test.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.test.tsx index bc56dc7a8a354..6a429d98756af 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.test.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.test.tsx @@ -35,7 +35,7 @@ describe('Ping Timestamp component', () => { (fetchStatus) => { jest .spyOn(observabilityPublic, 'useFetcher') - .mockReturnValue({ status: fetchStatus, data: null, refetch: () => null }); + .mockReturnValue({ status: fetchStatus, data: null, refetch: () => null, loading: true }); const { getByTestId } = render( ); @@ -48,7 +48,11 @@ describe('Ping Timestamp component', () => { .spyOn(observabilityPublic, 'useFetcher') .mockReturnValue({ status: FETCH_STATUS.SUCCESS, data: null, refetch: () => null }); const { getByTestId } = render( - + ); expect(getByTestId('pingTimestampNoImageAvailable')).toBeInTheDocument(); }); diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx index 3bc443426d523..3058aaacb4d85 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx @@ -10,14 +10,12 @@ import useIntersection from 'react-use/lib/useIntersection'; import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { useFetcher, FETCH_STATUS } from '@kbn/observability-plugin/public'; +import { useInProgressImage } from './use_in_progress_image'; import { isScreenshotImageBlob, isScreenshotRef, - ScreenshotImageBlob, ScreenshotRefImageData, } from '../../../../../../../common/runtime_types'; -import { getJourneyScreenshot } from '../../../../../state/api/journey'; import { UptimeSettingsContext } from '../../../../../contexts'; import { NoImageDisplay } from './no_image_display'; @@ -38,9 +36,16 @@ interface Props { label?: string; stepStatus?: string; initialStepNo?: number; + allStepsLoaded?: boolean; } -export const PingTimestamp = ({ label, checkGroup, stepStatus, initialStepNo = 1 }: Props) => { +export const PingTimestamp = ({ + label, + checkGroup, + stepStatus, + allStepsLoaded, + initialStepNo = 1, +}: Props) => { const [stepNumber, setStepNumber] = useState(initialStepNo); const [isImagePopoverOpen, setIsImagePopoverOpen] = useState(false); @@ -58,18 +63,15 @@ export const PingTimestamp = ({ label, checkGroup, stepStatus, initialStepNo = 1 threshold: 1, }); - const { data, status } = useFetcher(() => { - if (stepStatus === 'skipped') { - return new Promise((resolve) => - resolve(null) - ); - } + const [screenshotRef, setScreenshotRef] = useState(undefined); - if (intersection && intersection.intersectionRatio === 1 && !stepImages[stepNumber - 1]) - return getJourneyScreenshot(imgPath); - }, [intersection?.intersectionRatio, stepNumber, imgPath]); + const { data, loading } = useInProgressImage({ + hasImage: Boolean(stepImages[stepNumber - 1]) || Boolean(screenshotRef), + hasIntersected: Boolean(intersection && intersection.intersectionRatio === 1), + stepStatus, + imgPath, + }); - const [screenshotRef, setScreenshotRef] = useState(undefined); useEffect(() => { if (isScreenshotRef(data)) { setScreenshotRef(data); @@ -95,7 +97,7 @@ export const PingTimestamp = ({ label, checkGroup, stepStatus, initialStepNo = 1 maxSteps={data?.maxSteps} setStepNumber={setStepNumber} stepNumber={stepNumber} - isLoading={status === FETCH_STATUS.LOADING || status === FETCH_STATUS.PENDING} + isLoading={Boolean(loading)} label={label} onVisible={(val) => setNumberOfCaptions((prevVal) => (val ? prevVal + 1 : prevVal - 1))} /> @@ -131,11 +133,7 @@ export const PingTimestamp = ({ label, checkGroup, stepStatus, initialStepNo = 1 /> )} {!imgSrc && !screenshotRef && ( - + )} diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/step_image_popover.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/step_image_popover.tsx index 1d97e3e4e248a..1bba981496b13 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/step_image_popover.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/step_image_popover.tsx @@ -5,9 +5,10 @@ * 2.0. */ -import { EuiImage, EuiLoadingSpinner, EuiPopover } from '@elastic/eui'; +import { EuiImage, EuiPopover } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; +import { LoadingImageState } from './no_image_available'; import { ScreenshotRefImageData } from '../../../../../../../common/runtime_types/ping/synthetics'; import { fullSizeImageAlt } from './translations'; import { useCompositeImage } from '../../../../../hooks/use_composite_image'; @@ -49,7 +50,7 @@ const DefaultImage: React.FC = ({ className="syntheticsStepImage" /> ) : ( - + ); /** @@ -155,7 +156,7 @@ export const StepImagePopover: React.FC = ({ style={{ height: POPOVER_IMG_HEIGHT, width: POPOVER_IMG_WIDTH, objectFit: 'contain' }} /> ) : ( - + )} ); diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/use_in_progress_image.ts b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/use_in_progress_image.ts new file mode 100644 index 0000000000000..67b319853eaaf --- /dev/null +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/use_in_progress_image.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useRouteMatch } from 'react-router-dom'; +import { useEffect, useState } from 'react'; +import { useFetcher } from '@kbn/observability-plugin/public'; +import { + ScreenshotImageBlob, + ScreenshotRefImageData, +} from '../../../../../../../common/runtime_types'; +import { getJourneyScreenshot } from '../../../../../state/api/journey'; +import { MONITOR_ADD_ROUTE, MONITOR_EDIT_ROUTE } from '../../../../../../../common/constants'; + +const NUMBER_OF_RETRIES = 20; + +export const useInProgressImage = ({ + stepStatus, + hasImage, + hasIntersected, + imgPath, +}: { + imgPath: string; + stepStatus?: string; + hasImage: boolean; + hasIntersected: boolean; +}) => { + const isAddRoute = useRouteMatch(MONITOR_ADD_ROUTE); + const isEditRoute = useRouteMatch(MONITOR_EDIT_ROUTE); + + const [retryLoading, setRetryLoading] = useState(0); + + const skippedStep = stepStatus === 'skipped'; + + const { data, loading } = useFetcher(() => { + if (skippedStep) { + return new Promise((resolve) => + resolve(null) + ); + } + + if (hasIntersected && !hasImage) return getJourneyScreenshot(imgPath); + }, [hasIntersected, imgPath, skippedStep, retryLoading]); + + useEffect(() => { + if (!loading && !hasImage && (isAddRoute?.isExact || isEditRoute?.isExact) && !skippedStep) { + setTimeout(() => { + setRetryLoading((prevState) => { + if (prevState < NUMBER_OF_RETRIES) { + return prevState + 1; + } + return prevState; + }); + }, 5 * 1000); + } + }, [hasImage, loading, isAddRoute?.isExact, isEditRoute?.isExact, skippedStep]); + + return { + data, + loading, + }; +}; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/test_now_mode/browser/browser_test_results.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/test_now_mode/browser/browser_test_results.tsx index 87eecddac3e65..c1aa80e88ccdc 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/test_now_mode/browser/browser_test_results.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/test_now_mode/browser/browser_test_results.tsx @@ -74,12 +74,13 @@ export const BrowserTestRunResult = ({ monitorId, isMonitorSaved, expectPings, o )} - {summaryDoc && completedSteps > 0 && ( + {completedSteps > 0 && ( )} diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/test_now_mode/browser/use_browser_run_once_monitors.ts b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/test_now_mode/browser/use_browser_run_once_monitors.ts index 034fad5b9a8d0..39215ab22fdf0 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/test_now_mode/browser/use_browser_run_once_monitors.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/test_now_mode/browser/use_browser_run_once_monitors.ts @@ -86,7 +86,7 @@ export const useBrowserRunOnceMonitors = ({ skipDetails?: boolean; expectSummaryDocs: number; }) => { - const { refreshTimer, lastRefresh } = useTickTick(3 * 1000, refresh); + const { refreshTimer, lastRefresh } = useTickTick(5 * 1000, refresh); const [checkGroupResults, setCheckGroupResults] = useState(() => { return new Array(expectSummaryDocs) @@ -260,9 +260,15 @@ function mergeCheckGroups(prev: CheckGroupResult, curr: Partial 0) { + steps = prev.steps; + } + return { ...(prev ?? {}), ...curr, + steps, completedSteps, }; } diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/empty_state/use_has_data.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/empty_state/use_has_data.tsx index 01ca9d9927e1b..671c11da398a9 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/empty_state/use_has_data.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/empty_state/use_has_data.tsx @@ -7,10 +7,12 @@ import { useContext, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { useRouteMatch } from 'react-router-dom'; import { indexStatusAction } from '../../../state/actions'; import { indexStatusSelector, selectDynamicSettings } from '../../../state/selectors'; import { UptimeRefreshContext } from '../../../contexts'; import { getDynamicSettings } from '../../../state/actions/dynamic_settings'; +import { MONITOR_ADD_ROUTE, MONITOR_EDIT_ROUTE } from '../../../../../common/constants'; export const useHasData = () => { const { loading, error, data } = useSelector(indexStatusSelector); @@ -20,9 +22,16 @@ export const useHasData = () => { const dispatch = useDispatch(); + const isAddRoute = useRouteMatch(MONITOR_ADD_ROUTE); + const isEditRoute = useRouteMatch(MONITOR_EDIT_ROUTE); + + const skippedRoute = isAddRoute?.isExact || isEditRoute?.isExact; + useEffect(() => { - dispatch(indexStatusAction.get()); - }, [dispatch, lastRefresh]); + if (!skippedRoute) { + dispatch(indexStatusAction.get()); + } + }, [dispatch, lastRefresh, skippedRoute]); useEffect(() => { dispatch(getDynamicSettings()); diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/synthetics/check_steps/step_image.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/synthetics/check_steps/step_image.tsx index f1280cfe368b1..7f310938fc88e 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/synthetics/check_steps/step_image.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/synthetics/check_steps/step_image.tsx @@ -12,10 +12,11 @@ import { PingTimestamp } from '../../monitor/ping_list/columns/ping_timestamp'; interface Props { step: JourneyStep; + allStepsLoaded?: boolean; compactView?: boolean; } -export const StepImage = ({ step, compactView }: Props) => { +export const StepImage = ({ step, compactView, allStepsLoaded }: Props) => { return ( @@ -23,6 +24,7 @@ export const StepImage = ({ step, compactView }: Props) => { checkGroup={step.monitor.check_group} initialStepNo={step.synthetics?.step?.index} stepStatus={step.synthetics.payload?.status} + allStepsLoaded={allStepsLoaded} /> diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/synthetics/check_steps/steps_list.test.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/synthetics/check_steps/steps_list.test.tsx index 1a792dfa9f8d3..00b40415adefb 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/synthetics/check_steps/steps_list.test.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/synthetics/check_steps/steps_list.test.tsx @@ -74,12 +74,14 @@ describe('StepList component', () => { }); it('creates expected message for all failed', () => { - const { getByText } = render(); + const { getByText } = render(); expect(getByText('2 Steps - all failed or skipped')); }); it('renders a link to the step detail view', () => { - const { getByTitle, getByTestId } = render(); + const { getByTitle, getByTestId } = render( + + ); expect(getByTestId('step-detail-link')).toHaveAttribute('href', '/journey/fake-group/step/1'); expect(forDesktopOnly(getByTitle, 'title')(`Failed`)); }); @@ -91,7 +93,7 @@ describe('StepList component', () => { ])('supplies status badge correct status', (status, expectedStatus) => { const step = steps[0]; step.synthetics!.payload!.status = status; - const { getByText } = render(); + const { getByText } = render(); expect(forDesktopOnly(getByText)(expectedStatus)); }); @@ -99,14 +101,14 @@ describe('StepList component', () => { steps[0].synthetics!.payload!.status = 'succeeded'; steps[1].synthetics!.payload!.status = 'succeeded'; - const { getByText } = render(); + const { getByText } = render(); expect(getByText('2 Steps - all succeeded')); }); it('creates appropriate message for mixed results', () => { steps[0].synthetics!.payload!.status = 'succeeded'; - const { getByText } = render(); + const { getByText } = render(); expect(getByText('2 Steps - 1 succeeded')); }); @@ -114,7 +116,7 @@ describe('StepList component', () => { steps[0].synthetics!.payload!.status = 'succeeded'; steps[1].synthetics!.payload!.status = 'skipped'; - const { getByText } = render(); + const { getByText } = render(); expect(getByText('2 Steps - 1 succeeded')); }); @@ -141,12 +143,14 @@ describe('StepList component', () => { }, }); - const { getByText } = render(); + const { getByText } = render(); expect(getByText('2 Steps - 1 succeeded')); }); it('renders a row per step', () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + ); expect(getByTestId('row-fake-group')); expect(getByTestId('row-fake-group-1')); }); @@ -158,13 +162,17 @@ describe('StepList component', () => { // rendered and its classes. it('renders the step name and index', () => { - const { getByText } = render(); + const { getByText } = render( + + ); expect(forMobileOnly(getByText)('1. load page')).toBeInTheDocument(); expect(forMobileOnly(getByText)('2. go to login')).toBeInTheDocument(); }); it('does not render the link to view step details', async () => { - const { queryByText } = render(); + const { queryByText } = render( + + ); expect(forMobileOnly(queryByText)(VIEW_PERFORMANCE)).not.toBeInTheDocument(); }); @@ -172,7 +180,9 @@ describe('StepList component', () => { steps[0].synthetics!.payload!.status = 'succeeded'; steps[1].synthetics!.payload!.status = 'skipped'; - const { getByText } = render(); + const { getByText } = render( + + ); expect(forMobileOnly(getByText)('Succeeded')).toBeInTheDocument(); expect(forMobileOnly(getByText)('Skipped')).toBeInTheDocument(); }); diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/synthetics/check_steps/steps_list.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/synthetics/check_steps/steps_list.tsx index d02b7ac0e51da..67da833384eb9 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/synthetics/check_steps/steps_list.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/synthetics/check_steps/steps_list.tsx @@ -36,6 +36,7 @@ interface Props { data: JourneyStep[]; error?: Error; loading: boolean; + allStepsLoaded: boolean; compactView?: boolean; showStepDurationTrend?: boolean; } @@ -90,6 +91,7 @@ export const StepsList = ({ data, error, loading, + allStepsLoaded, showStepDurationTrend = true, compactView = false, }: Props) => { @@ -126,7 +128,9 @@ export const StepsList = ({ align: 'left', field: 'timestamp', name: STEP_NAME_LABEL, - render: (_timestamp: string, item) => , + render: (_timestamp: string, item) => ( + + ), mobileOptions: { render: (item: JourneyStep) => ( diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/pages/synthetics/synthetics_checks.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/pages/synthetics/synthetics_checks.tsx index 276162d488a22..cc0df9b4c1687 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/pages/synthetics/synthetics_checks.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/pages/synthetics/synthetics_checks.tsx @@ -38,7 +38,7 @@ export const SyntheticsCheckSteps: React.FC = () => { return ( <> - + {(!steps || steps.length === 0) && !loading && } ); From 25d51f359114a627b74b1eeefe5e13200510f8e9 Mon Sep 17 00:00:00 2001 From: Or Ouziel Date: Mon, 13 Jun 2022 14:17:28 +0300 Subject: [PATCH 21/62] [Cloud Posture] fix flaky test (#134013) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../findings_by_resource_table.test.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.test.tsx index 6b712e6a83a51..6728c1fb974f9 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.test.tsx @@ -22,12 +22,14 @@ const getFakeFindingsByResource = (): FindingsByResourcePage => { const total = chance.integer() + count + 1; const normalized = count / total; + const [resourceName, resourceSubtype, ...cisSections] = chance.unique(chance.word, 4); + return { cluster_id: chance.guid(), resource_id: chance.guid(), - resource_name: chance.word(), - resource_subtype: chance.word(), - cis_sections: [chance.word(), chance.word()], + resource_name: resourceName, + resource_subtype: resourceSubtype, + cis_sections: cisSections, failed_findings: { count, normalized, From adfb8bfa88c673dc915729301e383f86809e85ca Mon Sep 17 00:00:00 2001 From: Kevin Lacabane Date: Mon, 13 Jun 2022 13:24:24 +0200 Subject: [PATCH 22/62] Truncate host names in Infrastructure metrics table (#134106) * truncate host names * add textOnly to container and pod Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../container/container_metrics_table.tsx | 1 + .../host/host_metrics_table.tsx | 1 + .../infrastructure_node_metrics_tables/pod/pod_metrics_table.tsx | 1 + 3 files changed, 3 insertions(+) diff --git a/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/container/container_metrics_table.tsx b/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/container/container_metrics_table.tsx index b7ba7e17915e4..d64659781dc0f 100644 --- a/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/container/container_metrics_table.tsx +++ b/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/container/container_metrics_table.tsx @@ -109,6 +109,7 @@ function containerNodeColumns( name: 'Name', field: 'name', truncateText: true, + textOnly: true, render: (name: string) => { return ; }, diff --git a/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/host/host_metrics_table.tsx b/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/host/host_metrics_table.tsx index 8df9c973e5a17..f0d77e84b4439 100644 --- a/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/host/host_metrics_table.tsx +++ b/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/host/host_metrics_table.tsx @@ -109,6 +109,7 @@ function hostMetricsColumns( name: 'Name', field: 'name', truncateText: true, + textOnly: true, render: (name: string) => ( ), diff --git a/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/pod/pod_metrics_table.tsx b/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/pod/pod_metrics_table.tsx index fa6d4b899f157..5d7801be3c930 100644 --- a/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/pod/pod_metrics_table.tsx +++ b/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/pod/pod_metrics_table.tsx @@ -107,6 +107,7 @@ function podNodeColumns( name: 'Name', field: 'name', truncateText: true, + textOnly: true, render: (name: string) => { return ; }, From d742d170bbecb70d7cc5186f64139417a6f6e792 Mon Sep 17 00:00:00 2001 From: Pete Hampton Date: Mon, 13 Jun 2022 12:46:19 +0100 Subject: [PATCH 23/62] Check if security user is operating in an airgapped network. (#134063) * Check if security user is operating in an airgapped network. * Fix up type. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/lib/telemetry/__mocks__/index.ts | 4 +- .../server/lib/telemetry/preview_sender.ts | 5 +- .../server/lib/telemetry/sender.test.ts | 18 ++++ .../server/lib/telemetry/sender.ts | 85 ++++++++++++++++++- .../server/lib/telemetry/task.test.ts | 16 ++-- .../server/lib/telemetry/task.ts | 6 ++ 6 files changed, 126 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts b/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts index 2cd95f0e58951..21337a297222a 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts @@ -14,7 +14,8 @@ import { PackagePolicy } from '@kbn/fleet-plugin/common/types/models/package_pol import { stubEndpointAlertResponse, stubProcessTree, stubFetchTimelineEvents } from './timeline'; export const createMockTelemetryEventsSender = ( - enableTelemetry?: boolean + enableTelemetry?: boolean, + canConnect?: boolean ): jest.Mocked => { return { setup: jest.fn(), @@ -25,6 +26,7 @@ export const createMockTelemetryEventsSender = ( queueTelemetryEvents: jest.fn(), processEvents: jest.fn(), isTelemetryOptedIn: jest.fn().mockReturnValue(enableTelemetry ?? jest.fn()), + isTelemetryServicesReachable: jest.fn().mockReturnValue(canConnect ?? jest.fn()), sendIfDue: jest.fn(), sendEvents: jest.fn(), sendOnDemand: jest.fn(), diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/preview_sender.ts b/x-pack/plugins/security_solution/server/lib/telemetry/preview_sender.ts index 6499d9328652c..6975c244af039 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/preview_sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/preview_sender.ts @@ -6,7 +6,6 @@ */ import axios, { AxiosInstance, AxiosResponse } from 'axios'; - import { Logger } from '@kbn/core/server'; import { TelemetryPluginStart, TelemetryPluginSetup } from '@kbn/telemetry-plugin/server'; import { UsageCounter } from '@kbn/usage-collection-plugin/server'; @@ -120,6 +119,10 @@ export class PreviewTelemetryEventsSender implements ITelemetryEventsSender { return this.composite.isTelemetryOptedIn(); } + public isTelemetryServicesReachable(): Promise { + return this.composite.isTelemetryServicesReachable(); + } + public sendIfDue(axiosInstance?: AxiosInstance): Promise { return this.composite.sendIfDue(axiosInstance); } diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts index 240634bccdca4..8e36dfd9268a5 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts @@ -211,6 +211,7 @@ describe('TelemetryEventsSender', () => { sender['telemetrySetup'] = { getTelemetryUrl: jest.fn(async () => new URL('https://telemetry.elastic.co')), }; + sender['isTelemetryServicesReachable'] = jest.fn(async () => true); sender['telemetryUsageCounter'] = telemetryUsageCounter; sender['sendEvents'] = jest.fn(async () => { sender['telemetryUsageCounter']?.incrementCounter({ @@ -249,6 +250,23 @@ describe('TelemetryEventsSender', () => { expect(sender['queue'].length).toBe(0); expect(sender['sendEvents']).toBeCalledTimes(0); }); + + it("shouldn't send when telemetry when opted in but cannot connect to elastic telemetry services", async () => { + const sender = new TelemetryEventsSender(logger); + sender['sendEvents'] = jest.fn(); + const telemetryStart = { + getIsOptedIn: jest.fn(async () => true), + }; + sender['telemetryStart'] = telemetryStart; + sender['isTelemetryServicesReachable'] = jest.fn(async () => false); + + sender.queueTelemetryEvents([{ 'event.kind': '1' }, { 'event.kind': '2' }]); + expect(sender['queue'].length).toBe(2); + await sender['sendIfDue'](); + + expect(sender['queue'].length).toBe(0); + expect(sender['sendEvents']).toBeCalledTimes(0); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts index 57f64b47f3df8..926aa46a242b7 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -46,6 +46,7 @@ export interface ITelemetryEventsSender { stop(): void; queueTelemetryEvents(events: TelemetryEvent[]): void; isTelemetryOptedIn(): Promise; + isTelemetryServicesReachable(): Promise; sendIfDue(axiosInstance?: AxiosInstance): Promise; processEvents(events: TelemetryEvent[]): TelemetryEvent[]; sendOnDemand(channel: string, toSend: unknown[], axiosInstance?: AxiosInstance): Promise; @@ -63,7 +64,10 @@ export class TelemetryEventsSender implements ITelemetryEventsSender { private isSending = false; private receiver: ITelemetryReceiver | undefined; private queue: TelemetryEvent[] = []; - private isOptedIn?: boolean = true; // Assume true until the first check + + // Assume both true until the first check + private isOptedIn?: boolean = true; + private isElasticTelemetryReachable?: boolean = true; private telemetryUsageCounter?: UsageCounter; private telemetryTasks?: SecurityTelemetryTask[]; @@ -158,6 +162,67 @@ export class TelemetryEventsSender implements ITelemetryEventsSender { return this.isOptedIn === true; } + /** + * Issue: https://github.com/elastic/kibana/issues/133321 + * + * As of 8.3 - Telemetry is opted in by default, but the Kibana instance may + * be deployed in a network where outbound connections are restricted. This + * causes hanging connections in backend telemetry code. A previous bugfix + * included a default timeout for the client, but this code shouldn't be + * reachable if we cannot connect to Elastic Telemetry Services. This + * function call can be utilized to check if the Kibana instance can + * call out. + * + * Please note that this function should be used with care. DO NOT call this + * function in a way that does not take into consideration if the deployment + * opted out of telemetry. For example, + * + * DO NOT + * -------- + * + * if (isTelemetryServicesReachable() && isTelemetryOptedIn()) { + * ... + * } + * + * DO + * -------- + * + * if (isTelemetryOptedIn() && isTelemetryServicesReachable()) { + * ... + * } + * + * Is ok because the call to `isTelemetryServicesReachable()` is never called + * because `isTelemetryOptedIn()` short-circuits the conditional. + * + * DO NOT + * -------- + * + * const [optedIn, isReachable] = await Promise.all([ + * isTelemetryOptedIn(), + * isTelemetryServicesReachable(), + * ]); + * + * As it does not take into consideration the execution order and makes a redundant + * network call to Elastic Telemetry Services. + * + * Staging URL: https://telemetry-staging.elastic.co/ping + * Production URL: https://telemetry.elastic.co/ping + */ + public async isTelemetryServicesReachable() { + try { + const telemetryUrl = await this.fetchTelemetryPingUrl(); + const resp = await axios.get(telemetryUrl, { timeout: 3000 }); + if (resp.status === 200) { + this.logger.debug('[Security Telemetry] elastic telemetry services are reachable'); + return true; + } + + return false; + } catch (_err) { + return false; + } + } + public async sendIfDue(axiosInstance: AxiosInstance = axios) { if (this.isSending) { return; @@ -178,6 +243,14 @@ export class TelemetryEventsSender implements ITelemetryEventsSender { return; } + this.isElasticTelemetryReachable = await this.isTelemetryServicesReachable(); + if (!this.isElasticTelemetryReachable) { + this.logger.debug(`Telemetry Services are not reachable.`); + this.queue = []; + this.isSending = false; + return; + } + const clusterInfo = this.receiver?.getClusterInfo(); const [telemetryUrl, licenseInfo] = await Promise.all([ @@ -282,6 +355,16 @@ export class TelemetryEventsSender implements ITelemetryEventsSender { return url.toString(); } + private async fetchTelemetryPingUrl(): Promise { + const telemetryUrl = await this.telemetrySetup?.getTelemetryUrl(); + if (!telemetryUrl) { + throw Error("Couldn't get telemetry URL"); + } + + telemetryUrl.pathname = `/ping`; + return telemetryUrl.toString(); + } + private async sendEvents( events: unknown[], telemetryUrl: string, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/task.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/task.test.ts index 36a93fd232bb7..8ff4eabe77df6 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/task.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/task.test.ts @@ -58,7 +58,7 @@ describe('test security telemetry task', () => { mockTelemetryTaskConfig, mockTelemetryEventsSender, mockTelemetryReceiver, - } = await testTelemetryTaskRun(true); + } = await testTelemetryTaskRun(true, true); expect(mockTelemetryTaskConfig.runTask).toHaveBeenCalledWith( telemetryTask.getTaskId(), @@ -72,19 +72,25 @@ describe('test security telemetry task', () => { ); }); - test('telemetry task should not run if opted out', async () => { - const { mockTelemetryTaskConfig } = await testTelemetryTaskRun(false); + test('security telemetry task should not run if opted out', async () => { + const { mockTelemetryTaskConfig } = await testTelemetryTaskRun(false, true); expect(mockTelemetryTaskConfig.runTask).not.toHaveBeenCalled(); }); - async function testTelemetryTaskRun(optedIn: boolean) { + test('security telemetry tasks should not run if opted in but cannot phone home', async () => { + const { mockTelemetryTaskConfig } = await testTelemetryTaskRun(true, false); + + expect(mockTelemetryTaskConfig.runTask).not.toHaveBeenCalled(); + }); + + async function testTelemetryTaskRun(optedIn: boolean, canConnect: boolean) { const now = new Date(); const testType = 'security:test-task'; const testLastTimestamp = now.toISOString(); const mockTaskManagerSetup = taskManagerMock.createSetup(); const mockTelemetryTaskConfig = createMockSecurityTelemetryTask(testType, testLastTimestamp); - const mockTelemetryEventsSender = createMockTelemetryEventsSender(optedIn); + const mockTelemetryEventsSender = createMockTelemetryEventsSender(optedIn, canConnect); const mockTelemetryReceiver = createMockTelemetryReceiver(); const telemetryTask = new SecurityTelemetryTask( mockTelemetryTaskConfig, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/task.ts b/x-pack/plugins/security_solution/server/lib/telemetry/task.ts index dc55d43bd8008..70e06c3d88dbe 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/task.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/task.ts @@ -142,6 +142,12 @@ export class SecurityTelemetryTask { return 0; } + const isTelemetryServicesReachable = await this.sender.isTelemetryServicesReachable(); + if (!isTelemetryServicesReachable) { + this.logger.debug(`[task ${taskId}]: cannot reach telemetry services`); + return 0; + } + this.logger.debug(`[task ${taskId}]: running task`); return this.config.runTask(taskId, this.logger, this.receiver, this.sender, executionPeriod); }; From 5c97d2bbf1546bb5d1289abb69c1bce82d4841ae Mon Sep 17 00:00:00 2001 From: Marta Bondyra <4283304+mbondyra@users.noreply.github.com> Date: Mon, 13 Jun 2022 14:46:16 +0200 Subject: [PATCH 24/62] [Lens] fix color picker switch for annotations (#134052) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../xy_config_panel/color_picker.tsx | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx index 82c3ed12a0185..2f3b00befc2d4 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx @@ -70,29 +70,34 @@ export const ColorPicker = ({ disabled?: boolean; showAlpha?: boolean; }) => { - const [color, setColor] = useState(overwriteColor || defaultColor); - const [hexColor, setHexColor] = useState(overwriteColor || defaultColor); - const [currentColorAlpha, setCurrentColorAlpha] = useState(getColorAlpha(color)); + const [colorText, setColorText] = useState(overwriteColor || defaultColor); + const [validatedColor, setValidatedColor] = useState(overwriteColor || defaultColor); + const [currentColorAlpha, setCurrentColorAlpha] = useState(getColorAlpha(colorText)); const unflushedChanges = useRef(false); useEffect(() => { // only the changes from outside the color picker should be applied if (!unflushedChanges.current) { // something external changed the color that is currently selected (switching from annotation line to annotation range) - if (overwriteColor && hexColor && overwriteColor !== hexColor) { - setColor(overwriteColor || defaultColor); + if ( + overwriteColor && + validatedColor && + overwriteColor.toUpperCase() !== validatedColor.toUpperCase() + ) { + setColorText(overwriteColor); + setValidatedColor(overwriteColor.toUpperCase()); setCurrentColorAlpha(getColorAlpha(overwriteColor)); } } unflushedChanges.current = false; - }, [hexColor, overwriteColor, defaultColor]); + }, [validatedColor, overwriteColor, defaultColor]); const handleColor: EuiColorPickerProps['onChange'] = (text, output) => { - setColor(text); + setColorText(text); unflushedChanges.current = true; if (output.isValid) { - setHexColor(output.hex); - setCurrentColorAlpha(chroma(output.hex)?.alpha() || 1); + setValidatedColor(output.hex.toUpperCase()); + setCurrentColorAlpha(getColorAlpha(output.hex)); setConfig({ color: output.hex }); } if (text === '') { @@ -113,7 +118,7 @@ export const ColorPicker = ({ compressed isClearable={Boolean(overwriteColor)} onChange={handleColor} - color={disabled ? '' : color} + color={disabled ? '' : colorText} disabled={disabled} placeholder={ defaultColor?.toUpperCase() || @@ -139,7 +144,7 @@ export const ColorPicker = ({ From a51e8e4eb2e0a0610d8af7f2c7d756077ec71999 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Mon, 13 Jun 2022 15:28:23 +0200 Subject: [PATCH 25/62] [ML] Disable the Single Metric Viewer button for not viewable jobs (#134048) * conditionally disable the SMV button * fix MlPageHeader * refactor * change a label for disabled smv button * fix types * assertSingleMetricViewerButtonEnabled * assert single metric viewer button disabled * fix tests * smv url test * pluralising the tooltip * use viewable detector by default * extra check --- .../anomaly_results_view_selector.tsx | 37 +++- .../components/ml_page/ml_page.tsx | 192 ++++++++---------- .../components/page_header/page_header.tsx | 31 ++- .../explorer/anomaly_explorer_common_state.ts | 14 +- .../application/explorer/explorer_utils.ts | 10 +- .../application/routing/routes/explorer.tsx | 5 +- .../routing/routes/timeseriesexplorer.tsx | 16 +- .../get_viewable_detectors.ts | 2 +- .../aggregated_scripted_job.ts | 14 +- .../ml/anomaly_detection/anomaly_explorer.ts | 6 +- .../services/ml/anomaly_explorer.ts | 12 ++ .../test/functional/services/ml/navigation.ts | 8 + 12 files changed, 207 insertions(+), 140 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.tsx b/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.tsx index 4ad4e09d01e1b..3db87633b807c 100644 --- a/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.tsx +++ b/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.tsx @@ -7,34 +7,53 @@ import React, { FC, useMemo } from 'react'; -import { EuiButtonGroup } from '@elastic/eui'; +import { EuiButtonGroup, EuiButtonGroupProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import type { ExplorerJob } from '../../explorer/explorer_utils'; import { useUrlState } from '../../util/url_state'; import { useMlLocator, useNavigateToPath } from '../../contexts/kibana'; import { ML_PAGES } from '../../../../common/constants/locator'; interface Props { viewId: typeof ML_PAGES.SINGLE_METRIC_VIEWER | typeof ML_PAGES.ANOMALY_EXPLORER; + selectedJobs?: ExplorerJob[] | null; } -// Component for rendering a set of buttons for switching between the Anomaly Detection results views. - -export const AnomalyResultsViewSelector: FC = ({ viewId }) => { +/** + * Component for rendering a set of buttons for switching between the Anomaly Detection results views. + */ +export const AnomalyResultsViewSelector: FC = ({ viewId, selectedJobs }) => { const locator = useMlLocator()!; const navigateToPath = useNavigateToPath(); - const toggleButtonsIcons = useMemo( + const smvJobs = (selectedJobs ?? []).filter((job) => job.isSingleMetricViewerJob); + const isSingleMetricViewerDisabled = smvJobs.length === 0; + + const toggleButtonsIcons = useMemo( () => [ { id: 'timeseriesexplorer', - label: i18n.translate('xpack.ml.anomalyResultsViewSelector.singleMetricViewerLabel', { - defaultMessage: 'View results in the Single Metric Viewer', - }), + label: + viewId === 'explorer' && isSingleMetricViewerDisabled + ? i18n.translate( + 'xpack.ml.anomalyResultsViewSelector.singleMetricViewerDisabledLabel', + { + defaultMessage: + 'Selected {jobsCount, plural, one {job is} other {jobs are}} not viewable in the Single Metric Viewer', + values: { + jobsCount: selectedJobs?.length ?? 0, + }, + } + ) + : i18n.translate('xpack.ml.anomalyResultsViewSelector.singleMetricViewerLabel', { + defaultMessage: 'View results in the Single Metric Viewer', + }), iconType: 'visLine', value: ML_PAGES.SINGLE_METRIC_VIEWER, 'data-test-subj': 'mlAnomalyResultsViewSelectorSingleMetricViewer', + isDisabled: viewId === 'explorer' && isSingleMetricViewerDisabled, }, { id: 'explorer', @@ -46,7 +65,7 @@ export const AnomalyResultsViewSelector: FC = ({ viewId }) => { 'data-test-subj': 'mlAnomalyResultsViewSelectorExplorer', }, ], - [] + [isSingleMetricViewerDisabled, selectedJobs?.length] ); const [globalState] = useUrlState('_g'); diff --git a/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx b/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx index 7602da6a6c4e3..90dd95dbafb71 100644 --- a/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx @@ -5,12 +5,14 @@ * 2.0. */ -import React, { createContext, FC, useCallback, useMemo, useReducer } from 'react'; -import { EuiLoadingContent, EuiPageContentBody } from '@elastic/eui'; +import React, { createContext, FC, useMemo, useState } from 'react'; +import { EuiPageContentBody } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Redirect, Route, Switch } from 'react-router-dom'; import type { AppMountParameters } from '@kbn/core/public'; import { KibanaPageTemplate, RedirectAppLinks } from '@kbn/kibana-react-plugin/public'; +import { createPortalNode, PortalNode } from 'react-reverse-portal'; +import { MlPageHeaderRenderer } from '../page_header/page_header'; import { useSideNavItems } from './side_nav'; import * as routes from '../../routing/routes'; import { MlPageWrapper } from '../../routing/ml_page_wrapper'; @@ -21,33 +23,16 @@ import { useActiveRoute } from '../../routing/use_active_route'; import { useDocTitle } from '../../routing/use_doc_title'; export const MlPageControlsContext = createContext<{ - setPageTitle: (v?: React.ReactNode | undefined) => void; + headerPortal: PortalNode; setHeaderActionMenu?: AppMountParameters['setHeaderActionMenu']; -}>({ setPageTitle: () => {}, setHeaderActionMenu: () => {} }); - -const ML_PAGE_ACTION = { - SET_HEADER: 'setPageHeader', -}; - -interface SetHeaderAction { - type: typeof ML_PAGE_ACTION.SET_HEADER; - payload: React.ReactNode; -} - -type PageAction = SetHeaderAction; - -interface MlPageUIState { - pageHeader?: React.ReactNode; -} - -function pageStateReducer(state: MlPageUIState, action: PageAction): MlPageUIState { - switch (action.type) { - case ML_PAGE_ACTION.SET_HEADER: - return { ...state, pageHeader: action.payload }; - } - - return state; -} + setIsHeaderMounted: (v: boolean) => void; + isHeaderMounted: boolean; +}>({ + setHeaderActionMenu: () => {}, + headerPortal: createPortalNode(), + isHeaderMounted: false, + setIsHeaderMounted: () => {}, +}); /** * Main page component of the ML App @@ -61,14 +46,8 @@ export const MlPage: FC<{ pageDeps: PageDependencies }> = React.memo(({ pageDeps }, } = useMlKibana(); - const [pageState, dispatch] = useReducer(pageStateReducer, {}); - - const setPageTitle = useCallback( - (payload) => { - dispatch({ type: ML_PAGE_ACTION.SET_HEADER, payload }); - }, - [dispatch] - ); + const headerPortalNode = useMemo(() => createPortalNode(), []); + const [isHeaderMounted, setIsHeaderMounted] = useState(false); const routeList = useMemo( () => @@ -87,81 +66,88 @@ export const MlPage: FC<{ pageDeps: PageDependencies }> = React.memo(({ pageDeps useDocTitle(activeRoute); return ( - , - rightSideItems, - restrictWidth: false, - }} - pageBodyProps={{ - 'data-test-subj': activeRoute?.['data-test-subj'], + - - + , + rightSideItems, + restrictWidth: false, + }} + pageBodyProps={{ + 'data-test-subj': activeRoute?.['data-test-subj'], + }} + > + + + ); }); interface CommonPageWrapperProps { - setPageTitle: (title?: React.ReactNode | undefined) => void; + setIsHeaderMounted: (v: boolean) => void; pageDeps: PageDependencies; routeList: MlRoute[]; + headerPortal: PortalNode; } -const CommonPageWrapper: FC = React.memo( - ({ setPageTitle, pageDeps, routeList }) => { - const { - services: { application }, - } = useMlKibana(); +const CommonPageWrapper: FC = React.memo(({ pageDeps, routeList }) => { + const { + services: { application }, + } = useMlKibana(); - return ( - /** RedirectAppLinks intercepts all
    tags to use navigateToUrl - * avoiding full page reload **/ - - - - - {routeList.map((route) => { - return ( - { - window.setTimeout(() => { - pageDeps.setBreadcrumbs(route.breadcrumbs); - }); - return ( - - {route.render(props, pageDeps)} - - ); - }} - /> - ); - })} - - - - - - ); - } -); + return ( + /** RedirectAppLinks intercepts all tags to use navigateToUrl + * avoiding full page reload **/ + + + + {routeList.map((route) => { + return ( + { + window.setTimeout(() => { + pageDeps.setBreadcrumbs(route.breadcrumbs); + }); + return ( + {route.render(props, pageDeps)} + ); + }} + /> + ); + })} + + + + + ); +}); diff --git a/x-pack/plugins/ml/public/application/components/page_header/page_header.tsx b/x-pack/plugins/ml/public/application/components/page_header/page_header.tsx index 9736fef72fa56..0ae998db158fe 100644 --- a/x-pack/plugins/ml/public/application/components/page_header/page_header.tsx +++ b/x-pack/plugins/ml/public/application/components/page_header/page_header.tsx @@ -5,23 +5,32 @@ * 2.0. */ -import React, { FC, useContext, useEffect, useMemo } from 'react'; -import { createPortalNode, InPortal } from 'react-reverse-portal'; +import React, { FC, useContext, useEffect } from 'react'; +import { InPortal, OutPortal } from 'react-reverse-portal'; +import { EuiLoadingContent } from '@elastic/eui'; import { MlPageControlsContext } from '../ml_page/ml_page'; +/** + * Component for setting the page header content. + */ export const MlPageHeader: FC = ({ children }) => { - const { setPageTitle } = useContext(MlPageControlsContext); - - const portalNode = useMemo(() => createPortalNode(), []); + const { headerPortal, setIsHeaderMounted } = useContext(MlPageControlsContext); useEffect(() => { - setPageTitle(children); - + setIsHeaderMounted(true); return () => { - portalNode.unmount(); - setPageTitle(undefined); + setIsHeaderMounted(false); }; - }, [portalNode, setPageTitle]); + }, []); + + return {children}; +}; + +/** + * Renders content of the {@link MlPageHeader} + */ +export const MlPageHeaderRenderer: FC = () => { + const { headerPortal, isHeaderMounted } = useContext(MlPageControlsContext); - return {children}; + return isHeaderMounted ? : ; }; diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_explorer_common_state.ts b/x-pack/plugins/ml/public/application/explorer/anomaly_explorer_common_state.ts index 45995fa94838c..2bed3763c70a3 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomaly_explorer_common_state.ts +++ b/x-pack/plugins/ml/public/application/explorer/anomaly_explorer_common_state.ts @@ -6,7 +6,7 @@ */ import { BehaviorSubject, Observable, Subscription } from 'rxjs'; -import { distinctUntilChanged, map, skipWhile } from 'rxjs/operators'; +import { distinctUntilChanged, map, shareReplay, skipWhile } from 'rxjs/operators'; import { isEqual } from 'lodash'; import type { ExplorerJob } from './explorer_utils'; import type { InfluencersFilterQuery } from '../../../common/types/es_client'; @@ -69,10 +69,20 @@ export class AnomalyExplorerCommonStateService extends StateService { public getSelectedJobs$(): Observable { return this._selectedJobs$.pipe( skipWhile((v) => !v || !v.length), - distinctUntilChanged(isEqual) + distinctUntilChanged(isEqual), + shareReplay(1) ); } + private readonly _smvJobs$ = this.getSelectedJobs$().pipe( + map((jobs) => jobs.filter((j) => j.isSingleMetricViewerJob)), + shareReplay(1) + ); + + public getSingleMetricJobs$(): Observable { + return this._smvJobs$; + } + public getSelectedJobs(): ExplorerJob[] | undefined { return this._selectedJobs$.getValue(); } diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts b/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts index 6aa3444c9caaf..818bc89b6c944 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts @@ -24,6 +24,7 @@ import { isSourceDataChartableForDetector, isModelPlotChartableForDetector, isModelPlotEnabled, + isTimeSeriesViewJob, } from '../../../common/util/job_utils'; import { parseInterval } from '../../../common/util/parse_interval'; import { ml } from '../services/ml_api_service'; @@ -49,6 +50,7 @@ export interface ExplorerJob { id: string; selected: boolean; bucketSpanSeconds: number; + isSingleMetricViewerJob?: boolean; } interface ClearedSelectedAnomaliesState { @@ -109,11 +111,15 @@ export interface ViewBySwimLaneData extends OverallSwimlaneData { } // create new job objects based on standard job config objects -// new job objects just contain job id, bucket span in seconds and a selected flag. export function createJobs(jobs: CombinedJob[]): ExplorerJob[] { return jobs.map((job) => { const bucketSpan = parseInterval(job.analysis_config.bucket_span); - return { id: job.job_id, selected: false, bucketSpanSeconds: bucketSpan!.asSeconds() }; + return { + id: job.job_id, + selected: false, + bucketSpanSeconds: bucketSpan!.asSeconds(), + isSingleMetricViewerJob: isTimeSeriesViewJob(job), + }; }); } diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index e67b793944e6b..744852279b413 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -250,7 +250,10 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim - + diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx index a7d8e9454f8a6..3e535bae1361e 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -12,6 +12,7 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; +import { getViewableDetectors } from '../../timeseriesexplorer/timeseriesexplorer_utils/get_viewable_detectors'; import { NavigateToPath, useNotifications } from '../../contexts/kibana'; import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; @@ -152,12 +153,6 @@ export const TimeSeriesExplorerUrlStateManager: FC { + await ml.navigation.navigateToSingleMetricViewer(testData.jobConfig.job_id); await ml.testExecution.logTestStep( 'should show warning message and ask to select another job' diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts index 3f143c0163fb3..64344713d1c2d 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts @@ -129,7 +129,7 @@ export default function ({ getService }: FtrProviderContext) { } } - await ml.testExecution.logTestStep('displays the swimlanes'); + await ml.testExecution.logTestStep('displays the swim lanes'); await ml.anomalyExplorer.assertOverallSwimlaneExists(); await ml.anomalyExplorer.assertSwimlaneViewByExists(); @@ -143,6 +143,10 @@ export default function ({ getService }: FtrProviderContext) { await ml.anomaliesTable.assertTableNotEmpty(); }); + it('has enabled Single Metric Viewer button', async () => { + await ml.anomalyExplorer.assertSingleMetricViewerButtonEnabled(true); + }); + it('renders Overall swim lane', async () => { await ml.testExecution.logTestStep('has correct axes labels'); // The showTimeline prop is set to false and no axis labels are rendered diff --git a/x-pack/test/functional/services/ml/anomaly_explorer.ts b/x-pack/test/functional/services/ml/anomaly_explorer.ts index 6aae8d0f9e02f..a9219dafcf950 100644 --- a/x-pack/test/functional/services/ml/anomaly_explorer.ts +++ b/x-pack/test/functional/services/ml/anomaly_explorer.ts @@ -176,5 +176,17 @@ export function MachineLearningAnomalyExplorerProvider({ async scrollMapContainerIntoView() { await testSubjects.scrollIntoView('mlAnomaliesMapContainer'); }, + + async assertSingleMetricViewerButtonEnabled(expectedEnabled = true) { + const isEnabled = await testSubjects.isEnabled( + 'mlAnomalyResultsViewSelectorSingleMetricViewer' + ); + expect(isEnabled).to.eql( + expectedEnabled, + `Expected the Single Metric Viewer button to be '${ + expectedEnabled ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, }; } diff --git a/x-pack/test/functional/services/ml/navigation.ts b/x-pack/test/functional/services/ml/navigation.ts index 4341fb1247119..2d0c1262b24ea 100644 --- a/x-pack/test/functional/services/ml/navigation.ts +++ b/x-pack/test/functional/services/ml/navigation.ts @@ -152,6 +152,14 @@ export function MachineLearningNavigationProvider({ await this.navigateToArea('~mlMainTab & ~anomalyDetection', 'mlPageJobManagement'); }, + async navigateToSingleMetricViewer(jobId: string) { + await PageObjects.common.navigateToUrlWithBrowserHistory( + 'ml', + `/timeseriesexplorer`, + `?_g=(ml%3A(jobIds%3A!(${jobId}))%2CrefreshInterval%3A(display%3AOff%2Cpause%3A!t%2Cvalue%3A0))` + ); + }, + async navigateToDataFrameAnalytics() { await this.navigateToArea('~mlMainTab & ~dataFrameAnalytics', 'mlPageDataFrameAnalytics'); }, From 180ec23920ac65219c664ada32bab048224f88ce Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Mon, 13 Jun 2022 16:42:50 +0300 Subject: [PATCH 26/62] [AggConfigs] Unify `createAggConfigs` methods (#133731) * [AggConfigs] Unify createAggConfigs methods * fix CI Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../data/common/query/timefilter/get_time.ts | 2 +- .../common/search/aggs/agg_configs.test.ts | 6 +- .../data/common/search/aggs/agg_configs.ts | 36 ++++--- .../search/aggs/agg_types_registry.test.ts | 55 ++++++++--- .../common/search/aggs/agg_types_registry.ts | 53 +++++----- .../common/search/aggs/aggs_service.test.ts | 34 ++----- .../data/common/search/aggs/aggs_service.ts | 51 ++++++---- .../test_helpers/mock_agg_types_registry.ts | 6 +- src/plugins/data/common/search/aggs/types.ts | 6 +- src/plugins/data/public/plugin.ts | 3 +- .../public/search/aggs/aggs_service.test.ts | 3 +- .../data/public/search/aggs/aggs_service.ts | 88 ++++------------- .../public/search/expressions/esaggs.test.ts | 10 +- .../data/public/search/expressions/esaggs.ts | 5 +- .../data/public/search/search_service.test.ts | 3 + .../data/public/search/search_service.ts | 16 +-- .../data/server/search/aggs/aggs_service.ts | 97 +++++-------------- src/plugins/data/server/search/aggs/types.ts | 4 +- .../server/search/expressions/esaggs.test.ts | 10 +- .../data/server/search/expressions/esaggs.ts | 6 +- .../data/server/search/search_service.ts | 4 +- .../public/embeddable/to_ast.ts | 21 ++-- 22 files changed, 235 insertions(+), 284 deletions(-) diff --git a/src/plugins/data/common/query/timefilter/get_time.ts b/src/plugins/data/common/query/timefilter/get_time.ts index f5adbc6cd001f..447d4b16e799f 100644 --- a/src/plugins/data/common/query/timefilter/get_time.ts +++ b/src/plugins/data/common/query/timefilter/get_time.ts @@ -14,7 +14,7 @@ import type { Moment } from 'moment'; import type { DataView } from '@kbn/data-views-plugin/common'; import type { TimeRange, TimeRangeBounds, RangeFilterParams } from '../..'; -interface CalculateBoundsOptions { +export interface CalculateBoundsOptions { forceNow?: Date; } diff --git a/src/plugins/data/common/search/aggs/agg_configs.test.ts b/src/plugins/data/common/search/aggs/agg_configs.test.ts index 3f629dc8d1be9..cd0495a3f78c6 100644 --- a/src/plugins/data/common/search/aggs/agg_configs.test.ts +++ b/src/plugins/data/common/search/aggs/agg_configs.test.ts @@ -813,8 +813,7 @@ describe('AggConfigs', () => { }); it('should generate the `metricsAtAllLevels` if hierarchical', () => { - const ac = new AggConfigs(indexPattern, [], { typesRegistry }, jest.fn()); - ac.hierarchical = true; + const ac = new AggConfigs(indexPattern, [], { typesRegistry, hierarchical: true }, jest.fn()); expect(toString(ac.toExpressionAst())).toMatchInlineSnapshot( `"esaggs index={indexPatternLoad id=\\"logstash-*\\"} metricsAtAllLevels=true partialRows=false"` @@ -822,8 +821,7 @@ describe('AggConfigs', () => { }); it('should generate the `partialRows` argument', () => { - const ac = new AggConfigs(indexPattern, [], { typesRegistry }, jest.fn()); - ac.partialRows = true; + const ac = new AggConfigs(indexPattern, [], { typesRegistry, partialRows: true }, jest.fn()); expect(toString(ac.toExpressionAst())).toMatchInlineSnapshot( `"esaggs index={indexPatternLoad id=\\"logstash-*\\"} metricsAtAllLevels=false partialRows=true"` diff --git a/src/plugins/data/common/search/aggs/agg_configs.ts b/src/plugins/data/common/search/aggs/agg_configs.ts index d22175ac7e4b6..65238ee7b4db1 100644 --- a/src/plugins/data/common/search/aggs/agg_configs.ts +++ b/src/plugins/data/common/search/aggs/agg_configs.ts @@ -9,14 +9,14 @@ import moment from 'moment-timezone'; import _, { cloneDeep } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { Assign } from '@kbn/utility-types'; +import type { Assign } from '@kbn/utility-types'; import { isRangeFilter } from '@kbn/es-query'; import type { DataView } from '@kbn/data-views-plugin/common'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { IndexPatternLoadExpressionFunctionDefinition } from '@kbn/data-views-plugin/common'; import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/common'; -import { +import type { IEsSearchResponse, ISearchOptions, ISearchSource, @@ -25,12 +25,12 @@ import { } from '../../../public'; import type { EsaggsExpressionFunctionDefinition } from '../expressions'; import { AggConfig, AggConfigSerialized, IAggConfig } from './agg_config'; -import { IAggType } from './agg_type'; -import { AggTypesRegistryStart } from './agg_types_registry'; +import type { IAggType } from './agg_type'; +import type { AggTypesRegistryStart } from './agg_types_registry'; import { AggGroupNames } from './agg_groups'; import { AggTypesDependencies, GetConfigFn, getUserTimeZone } from '../..'; import { TimeRange, getTime, calculateBounds } from '../..'; -import { IBucketAggConfig } from './buckets'; +import type { IBucketAggConfig } from './buckets'; import { insertTimeShiftSplit, mergeTimeShifts } from './utils/time_splits'; function removeParentAggs(obj: any) { @@ -88,8 +88,6 @@ export class AggConfigs { public timeFields?: string[]; public forceNow?: Date; public aggs: IAggConfig[] = []; - public partialRows?: boolean; - public hierarchical?: boolean; public readonly timeZone: string; constructor( @@ -98,9 +96,6 @@ export class AggConfigs { private opts: AggConfigsOptions, private getConfig: GetConfigFn ) { - this.hierarchical = opts.hierarchical ?? false; - this.partialRows = opts.partialRows ?? false; - this.timeZone = getUserTimeZone( this.getConfig, opts?.aggExecutionContext?.shouldDetectTimeZone @@ -110,6 +105,14 @@ export class AggConfigs { configStates.forEach((params: any) => this.createAggConfig(params)); } + public get hierarchical() { + return this.opts.hierarchical ?? false; + } + + public get partialRows() { + return this.opts.partialRows ?? false; + } + setTimeFields(timeFields: string[] | undefined) { this.timeFields = timeFields; } @@ -149,7 +152,13 @@ export class AggConfigs { } // clone method will reuse existing AggConfig in the list (will not create new instances) - clone({ enabledOnly = true } = {}) { + clone({ + enabledOnly = true, + opts, + }: { + enabledOnly?: boolean; + opts?: Partial; + } = {}) { const filterAggs = (agg: AggConfig) => { if (!enabledOnly) return true; return agg.enabled; @@ -160,8 +169,7 @@ export class AggConfigs { this.aggs.filter(filterAggs), { ...this.opts, - hierarchical: this.hierarchical, - partialRows: this.partialRows, + ...opts, }, this.getConfig ); @@ -440,7 +448,7 @@ export class AggConfigs { ]; } - postFlightTransform(response: IEsSearchResponse) { + postFlightTransform(response: IEsSearchResponse) { if (!this.hasTimeShifts()) { return response; } diff --git a/src/plugins/data/common/search/aggs/agg_types_registry.test.ts b/src/plugins/data/common/search/aggs/agg_types_registry.test.ts index 7400be22f5771..10dee88f48ae4 100644 --- a/src/plugins/data/common/search/aggs/agg_types_registry.test.ts +++ b/src/plugins/data/common/search/aggs/agg_types_registry.test.ts @@ -7,11 +7,32 @@ */ import { AggTypesRegistry, AggTypesRegistrySetup } from './agg_types_registry'; -import { BucketAggType } from './buckets/bucket_agg_type'; -import { MetricAggType } from './metrics/metric_agg_type'; +import type { BucketAggType } from './buckets/bucket_agg_type'; +import type { MetricAggType } from './metrics/metric_agg_type'; +import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks'; +import type { AggTypesDependencies } from './agg_types'; -const bucketType = () => ({ name: 'terms', type: 'buckets' } as BucketAggType); -const metricType = () => ({ name: 'count', type: 'metrics' } as MetricAggType); +/** @internal */ +export function mockGetFieldFormatsStart() { + const { deserialize, getDefaultInstance } = fieldFormatsMock; + return { + deserialize, + getDefaultInstance, + }; +} + +// Mocked uiSettings shared among aggs unit tests +const mockGetConfig = jest.fn((key) => key); + +/** @internal */ +export const mockAggTypesDependencies: AggTypesDependencies = { + calculateBounds: jest.fn(), + getFieldFormatsStart: mockGetFieldFormatsStart, + getConfig: mockGetConfig, +}; + +const bucketType = { name: 'terms', type: 'buckets' } as BucketAggType; +const metricType = { name: 'count', type: 'metrics' } as MetricAggType; describe('AggTypesRegistry', () => { let registry: AggTypesRegistry; @@ -21,18 +42,22 @@ describe('AggTypesRegistry', () => { beforeEach(() => { registry = new AggTypesRegistry(); setup = registry.setup(); - start = registry.start(); + start = registry.start({ + calculateBounds: jest.fn(), + getFieldFormatsStart: mockGetFieldFormatsStart, + getConfig: mockGetConfig, + }); }); it('registerBucket adds new buckets', () => { - setup.registerBucket('terms', bucketType); + setup.registerBucket('terms', () => bucketType); expect(start.getAll().buckets).toEqual([bucketType]); }); it('registerBucket throws error when registering duplicate bucket', () => { expect(() => { - setup.registerBucket('terms', bucketType); - setup.registerBucket('terms', bucketType); + setup.registerBucket('terms', () => bucketType); + setup.registerBucket('terms', () => bucketType); }).toThrow(/already been registered with name: terms/); const fooBucket = () => ({ name: 'foo', type: 'buckets' } as BucketAggType); @@ -44,14 +69,14 @@ describe('AggTypesRegistry', () => { }); it('registerMetric adds new metrics', () => { - setup.registerMetric('count', metricType); + setup.registerMetric('count', () => metricType); expect(start.getAll().metrics).toEqual([metricType]); }); it('registerMetric throws error when registering duplicate metric', () => { expect(() => { - setup.registerMetric('count', metricType); - setup.registerMetric('count', metricType); + setup.registerMetric('count', () => metricType); + setup.registerMetric('count', () => metricType); }).toThrow(/already been registered with name: count/); const fooBucket = () => ({ name: 'foo', type: 'buckets' } as BucketAggType); @@ -63,15 +88,15 @@ describe('AggTypesRegistry', () => { }); it('gets either buckets or metrics by id', () => { - setup.registerBucket('terms', bucketType); - setup.registerMetric('count', metricType); + setup.registerBucket('terms', () => bucketType); + setup.registerMetric('count', () => metricType); expect(start.get('terms')).toEqual(bucketType); expect(start.get('count')).toEqual(metricType); }); it('getAll returns all buckets and metrics', () => { - setup.registerBucket('terms', bucketType); - setup.registerMetric('count', metricType); + setup.registerBucket('terms', () => bucketType); + setup.registerMetric('count', () => metricType); expect(start.getAll()).toEqual({ buckets: [bucketType], metrics: [metricType], diff --git a/src/plugins/data/common/search/aggs/agg_types_registry.ts b/src/plugins/data/common/search/aggs/agg_types_registry.ts index 4e57b4db3fb50..24521a8d564ef 100644 --- a/src/plugins/data/common/search/aggs/agg_types_registry.ts +++ b/src/plugins/data/common/search/aggs/agg_types_registry.ts @@ -6,21 +6,12 @@ * Side Public License, v 1. */ -import { BucketAggType } from './buckets/bucket_agg_type'; -import { MetricAggType } from './metrics/metric_agg_type'; -import { AggTypesDependencies } from './agg_types'; +import type { BucketAggType } from './buckets/bucket_agg_type'; +import type { MetricAggType } from './metrics/metric_agg_type'; +import type { AggTypesDependencies } from './agg_types'; export type AggTypesRegistrySetup = ReturnType; -/** - * AggsCommonStart returns the _unitialized_ agg type providers, but in our - * real start contract we will need to return the initialized versions. - * So we need to provide the correct typings so they can be overwritten - * on client/server. - */ -export interface AggTypesRegistryStart { - get: (id: string) => BucketAggType | MetricAggType; - getAll: () => { buckets: Array>; metrics: Array> }; -} +export type AggTypesRegistryStart = ReturnType; export class AggTypesRegistry { private readonly bucketAggs = new Map(); @@ -38,6 +29,7 @@ export class AggTypesRegistry { if (this.bucketAggs.get(name) || this.metricAggs.get(name)) { throw new Error(`Agg has already been registered with name: ${name}`); } + this.bucketAggs.set(name, type); }, registerMetric: < @@ -55,17 +47,32 @@ export class AggTypesRegistry { }; }; - start = () => { + start = (aggTypesDependencies: AggTypesDependencies) => { + const initializedAggTypes = new Map(); + + const getInitializedFromCache = (key: string, agg: any): T => { + if (initializedAggTypes.has(key)) { + return initializedAggTypes.get(key); + } + const initialized = agg(aggTypesDependencies); + initializedAggTypes.set(key, initialized); + return initialized; + }; + return { - get: (name: string) => { - return this.bucketAggs.get(name) || this.metricAggs.get(name); - }, - getAll: () => { - return { - buckets: Array.from(this.bucketAggs.values()), - metrics: Array.from(this.metricAggs.values()), - }; - }, + get: (name: string) => + getInitializedFromCache | MetricAggType>( + name, + this.bucketAggs.get(name) || this.metricAggs.get(name) + ), + getAll: () => ({ + buckets: Array.from(this.bucketAggs.entries()).map(([key, value]) => + getInitializedFromCache>(key, value) + ), + metrics: Array.from(this.metricAggs.entries()).map(([key, value]) => + getInitializedFromCache>(key, value) + ), + }), }; }; } diff --git a/src/plugins/data/common/search/aggs/aggs_service.test.ts b/src/plugins/data/common/search/aggs/aggs_service.test.ts index 5f74605b0d3a5..ee40a1d76d1aa 100644 --- a/src/plugins/data/common/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/common/search/aggs/aggs_service.test.ts @@ -11,7 +11,7 @@ import { AggsCommonSetupDependencies, AggsCommonStartDependencies, } from './aggs_service'; -import { AggTypesDependencies, getAggTypes } from './agg_types'; +import { getAggTypes } from './agg_types'; import { BucketAggType } from './buckets/bucket_agg_type'; import { MetricAggType } from './metrics/metric_agg_type'; @@ -19,11 +19,6 @@ describe('Aggs service', () => { let service: AggsCommonService; let setupDeps: AggsCommonSetupDependencies; let startDeps: AggsCommonStartDependencies; - const aggTypesDependencies: AggTypesDependencies = { - calculateBounds: jest.fn(), - getFieldFormatsStart: jest.fn(), - getConfig: jest.fn(), - }; beforeEach(() => { service = new AggsCommonService(); @@ -33,7 +28,7 @@ describe('Aggs service', () => { startDeps = { getConfig: jest.fn(), getIndexPattern: jest.fn(), - }; + } as unknown as AggsCommonStartDependencies; }); describe('setup()', () => { @@ -56,8 +51,7 @@ describe('Aggs service', () => { () => ({ name: 'foo', type: 'buckets' } as BucketAggType) ); const aStart = a.start(startDeps); - expect(aStart.types.getAll().buckets.map((t) => t(aggTypesDependencies).name)) - .toMatchInlineSnapshot(` + expect(aStart.types.getAll().buckets.map((t) => t.name)).toMatchInlineSnapshot(` Array [ "date_histogram", "histogram", @@ -78,8 +72,7 @@ describe('Aggs service', () => { "foo", ] `); - expect(aStart.types.getAll().metrics.map((t) => t(aggTypesDependencies).name)) - .toMatchInlineSnapshot(` + expect(aStart.types.getAll().metrics.map((t) => t.name)).toMatchInlineSnapshot(` Array [ "count", "avg", @@ -111,8 +104,7 @@ describe('Aggs service', () => { b.setup(bSetupDeps); const bStart = b.start(startDeps); - expect(bStart.types.getAll().buckets.map((t) => t(aggTypesDependencies).name)) - .toMatchInlineSnapshot(` + expect(bStart.types.getAll().buckets.map((t) => t.name)).toMatchInlineSnapshot(` Array [ "date_histogram", "histogram", @@ -132,8 +124,7 @@ describe('Aggs service', () => { "diversified_sampler", ] `); - expect(bStart.types.getAll().metrics.map((t) => t(aggTypesDependencies).name)) - .toMatchInlineSnapshot(` + expect(bStart.types.getAll().metrics.map((t) => t.name)).toMatchInlineSnapshot(` Array [ "count", "avg", @@ -187,13 +178,9 @@ describe('Aggs service', () => { const aggTypes = getAggTypes(); expect(start.types.getAll().buckets.length).toBe(aggTypes.buckets.length + 1); - expect(start.types.getAll().buckets.some((t) => t(aggTypesDependencies).name === 'foo')).toBe( - true - ); + expect(start.types.getAll().buckets.some((t) => t.name === 'foo')).toBe(true); expect(start.types.getAll().metrics.length).toBe(aggTypes.metrics.length + 1); - expect(start.types.getAll().metrics.some((t) => t(aggTypesDependencies).name === 'bar')).toBe( - true - ); + expect(start.types.getAll().metrics.some((t) => t.name === 'bar')).toBe(true); }); test('registers all agg type expression functions', () => { @@ -214,11 +201,10 @@ describe('Aggs service', () => { expect(start).toHaveProperty('types'); }); - test('types registry returns uninitialized type providers', () => { + test('types registry returns initialized type providers', () => { service.setup(setupDeps); const start = service.start(startDeps); - expect(typeof start.types.get('terms')).toBe('function'); - expect(start.types.get('terms')(aggTypesDependencies).name).toBe('terms'); + expect(start.types.get('terms').name).toBe('terms'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/aggs_service.ts b/src/plugins/data/common/search/aggs/aggs_service.ts index 694be7019fa55..f88138d04f31a 100644 --- a/src/plugins/data/common/search/aggs/aggs_service.ts +++ b/src/plugins/data/common/search/aggs/aggs_service.ts @@ -8,7 +8,9 @@ import { ExpressionsServiceSetup } from '@kbn/expressions-plugin/common'; import type { DataView } from '@kbn/data-views-plugin/common'; -import { CreateAggConfigParams, UI_SETTINGS, AggTypesDependencies } from '../..'; +import type { FieldFormatsStartCommon } from '@kbn/field-formats-plugin/common'; +import { CalculateBoundsOptions } from '../../query'; +import { UI_SETTINGS, AggTypesDependencies, calculateBounds } from '../..'; import { GetConfigFn } from '../../types'; import { AggConfigs, @@ -37,9 +39,10 @@ export interface AggsCommonSetupDependencies { } export interface AggsCommonStartDependencies { - getConfig: GetConfigFn; getIndexPattern(id: string): Promise; - aggExecutionContext?: AggTypesDependencies['aggExecutionContext']; + getConfig: GetConfigFn; + fieldFormats: FieldFormatsStartCommon; + calculateBoundsOptions: CalculateBoundsOptions; } /** @@ -50,6 +53,8 @@ export interface AggsCommonStartDependencies { export class AggsCommonService { private readonly aggTypesRegistry = new AggTypesRegistry(); + constructor(private aggExecutionContext?: AggTypesDependencies['aggExecutionContext']) {} + public setup({ registerFunction }: AggsCommonSetupDependencies): AggsCommonSetup { const aggTypesSetup = this.aggTypesRegistry.setup(); @@ -67,26 +72,32 @@ export class AggsCommonService { }; } - public start({ getConfig, aggExecutionContext }: AggsCommonStartDependencies): AggsCommonStart { - const aggTypesStart = this.aggTypesRegistry.start(); - const calculateAutoTimeExpression = getCalculateAutoTimeExpression(getConfig); - - const createAggConfigs = (indexPattern: DataView, configStates?: CreateAggConfigParams[]) => { - return new AggConfigs( - indexPattern, - configStates, - { - typesRegistry: aggTypesStart, - aggExecutionContext, - }, - getConfig - ); - }; + public start({ + getConfig, + fieldFormats, + calculateBoundsOptions, + }: AggsCommonStartDependencies): AggsCommonStart { + const aggTypesStart = this.aggTypesRegistry.start({ + getConfig, + getFieldFormatsStart: () => fieldFormats, + aggExecutionContext: this.aggExecutionContext, + calculateBounds: (timeRange) => calculateBounds(timeRange, calculateBoundsOptions), + }); return { - calculateAutoTimeExpression, - createAggConfigs, types: aggTypesStart, + calculateAutoTimeExpression: getCalculateAutoTimeExpression(getConfig), + createAggConfigs: (indexPattern, configStates, options) => + new AggConfigs( + indexPattern, + configStates, + { + ...options, + typesRegistry: aggTypesStart, + aggExecutionContext: this.aggExecutionContext, + }, + getConfig + ), }; } } diff --git a/src/plugins/data/common/search/aggs/test_helpers/mock_agg_types_registry.ts b/src/plugins/data/common/search/aggs/test_helpers/mock_agg_types_registry.ts index 578e66222e01b..a7974acccd2d3 100644 --- a/src/plugins/data/common/search/aggs/test_helpers/mock_agg_types_registry.ts +++ b/src/plugins/data/common/search/aggs/test_helpers/mock_agg_types_registry.ts @@ -69,15 +69,15 @@ export function mockAggTypesRegistry(deps?: AggTypesDependencies): AggTypesRegis aggTypes.buckets.forEach(({ name, fn }) => registrySetup.registerBucket(name, fn)); aggTypes.metrics.forEach(({ name, fn }) => registrySetup.registerMetric(name, fn)); - const registryStart = registry.start(); + const registryStart = registry.start(deps ?? mockAggTypesDependencies); // initialize each agg type and store in memory registryStart.getAll().buckets.forEach((type) => { - const agg = type(deps ?? mockAggTypesDependencies); + const agg = type; initializedAggTypes.set(agg.name, agg); }); registryStart.getAll().metrics.forEach((type) => { - const agg = type(deps ?? mockAggTypesDependencies); + const agg = type; initializedAggTypes.set(agg.name, agg); }); diff --git a/src/plugins/data/common/search/aggs/types.ts b/src/plugins/data/common/search/aggs/types.ts index 0b0701cc46eaf..7e72365f06af0 100644 --- a/src/plugins/data/common/search/aggs/types.ts +++ b/src/plugins/data/common/search/aggs/types.ts @@ -91,6 +91,7 @@ import { aggFilteredMetric, aggSinglePercentile, aggSinglePercentileRank, + AggConfigsOptions, } from '.'; import { AggParamsSampler } from './buckets/sampler'; import { AggParamsDiversifiedSampler } from './buckets/diversified_sampler'; @@ -100,7 +101,7 @@ import { aggTopMetrics } from './metrics/top_metrics_fn'; import { AggParamsCount } from './metrics'; export type { IAggConfig, AggConfigSerialized } from './agg_config'; -export type { CreateAggConfigParams, IAggConfigs } from './agg_configs'; +export type { CreateAggConfigParams, IAggConfigs, AggConfigsOptions } from './agg_configs'; export type { IAggType } from './agg_type'; export type { AggParam, AggParamOption } from './agg_params'; export type { IFieldParamType } from './param_types'; @@ -116,7 +117,8 @@ export interface AggsCommonStart { calculateAutoTimeExpression: ReturnType; createAggConfigs: ( indexPattern: DataView, - configStates?: CreateAggConfigParams[] + configStates?: CreateAggConfigParams[], + options?: Partial ) => InstanceType; types: ReturnType; } diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 0790df6a95df2..d0a603b52b921 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -84,8 +84,8 @@ export class DataPublicPlugin bfetch, usageCollection, expressions, - nowProvider: this.nowProvider, management, + nowProvider: this.nowProvider, }); const queryService = this.queryService.setup({ @@ -137,6 +137,7 @@ export class DataPublicPlugin fieldFormats, indexPatterns: dataViews, screenshotMode, + nowProvider: this.nowProvider, }); setSearchService(search); diff --git a/src/plugins/data/public/search/aggs/aggs_service.test.ts b/src/plugins/data/public/search/aggs/aggs_service.test.ts index cc193b9cef15a..c391c023b2d27 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.test.ts @@ -34,12 +34,11 @@ describe('AggsService - public', () => { setupDeps = { registerFunction: expressionsPluginMock.createSetupContract().registerFunction, uiSettings, - nowProvider: createNowProviderMock(), }; startDeps = { fieldFormats: fieldFormatsServiceMock.createStartContract(), indexPatterns: dataPluginMock.createStartContract().indexPatterns, - uiSettings, + nowProvider: createNowProviderMock(), }; }); diff --git a/src/plugins/data/public/search/aggs/aggs_service.ts b/src/plugins/data/public/search/aggs/aggs_service.ts index 5a84c03ada672..640bb954561a1 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.ts @@ -12,13 +12,10 @@ import type { IUiSettingsClient } from '@kbn/core/public'; import type { ExpressionsServiceSetup } from '@kbn/expressions-plugin/common'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import type { DataViewsContract } from '@kbn/data-views-plugin/common'; -import { calculateBounds, TimeRange } from '../../../common'; import { - AggConfigs, aggsRequiredUiSettings, AggsCommonStartDependencies, AggsCommonService, - AggTypesDependencies, } from '../../../common/search/aggs'; import type { AggsSetup, AggsStart } from './types'; import type { NowProviderInternalContract } from '../../now_provider'; @@ -50,16 +47,15 @@ export function createGetConfig( /** @internal */ export interface AggsSetupDependencies { - registerFunction: ExpressionsServiceSetup['registerFunction']; uiSettings: IUiSettingsClient; - nowProvider: NowProviderInternalContract; + registerFunction: ExpressionsServiceSetup['registerFunction']; } /** @internal */ export interface AggsStartDependencies { fieldFormats: FieldFormatsStart; - uiSettings: IUiSettingsClient; indexPatterns: DataViewsContract; + nowProvider: NowProviderInternalContract; } /** @@ -68,84 +64,34 @@ export interface AggsStartDependencies { * output the correct DSL when you are ready to send your request to ES. */ export class AggsService { - private readonly aggsCommonService = new AggsCommonService(); - private readonly initializedAggTypes = new Map(); + private readonly aggsCommonService = new AggsCommonService({ + shouldDetectTimeZone: true, + }); private getConfig?: AggsCommonStartDependencies['getConfig']; private subscriptions: Subscription[] = []; - private nowProvider!: NowProviderInternalContract; - /** - * NowGetter uses window.location, so we must have a separate implementation - * of calculateBounds on the client and the server. - */ - private calculateBounds = (timeRange: TimeRange) => - calculateBounds(timeRange, { forceNow: this.nowProvider.get() }); - - public setup({ registerFunction, uiSettings, nowProvider }: AggsSetupDependencies): AggsSetup { - this.nowProvider = nowProvider; + public setup({ registerFunction, uiSettings }: AggsSetupDependencies): AggsSetup { this.getConfig = createGetConfig(uiSettings, aggsRequiredUiSettings, this.subscriptions); - return this.aggsCommonService.setup({ registerFunction }); + return this.aggsCommonService.setup({ + registerFunction, + }); } - public start({ fieldFormats, indexPatterns }: AggsStartDependencies): AggsStart { - const aggExecutionContext: AggTypesDependencies['aggExecutionContext'] = { - shouldDetectTimeZone: true, - }; - - const { calculateAutoTimeExpression, types } = this.aggsCommonService.start({ + public start({ indexPatterns, fieldFormats, nowProvider }: AggsStartDependencies): AggsStart { + const { calculateAutoTimeExpression, types, createAggConfigs } = this.aggsCommonService.start({ getConfig: this.getConfig!, getIndexPattern: indexPatterns.get, - aggExecutionContext, - }); - - const aggTypesDependencies: AggTypesDependencies = { - calculateBounds: this.calculateBounds, - getConfig: this.getConfig!, - getFieldFormatsStart: () => ({ - deserialize: fieldFormats.deserialize, - getDefaultInstance: fieldFormats.getDefaultInstance, - }), - aggExecutionContext, - }; - - // initialize each agg type and store in memory - types.getAll().buckets.forEach((type) => { - const agg = type(aggTypesDependencies); - this.initializedAggTypes.set(agg.name, agg); - }); - types.getAll().metrics.forEach((type) => { - const agg = type(aggTypesDependencies); - this.initializedAggTypes.set(agg.name, agg); - }); - - const typesRegistry = { - get: (name: string) => { - return this.initializedAggTypes.get(name); - }, - getAll: () => { - return { - buckets: Array.from(this.initializedAggTypes.values()).filter( - (agg) => agg.type === 'buckets' - ), - metrics: Array.from(this.initializedAggTypes.values()).filter( - (agg) => agg.type === 'metrics' - ), - }; + fieldFormats, + calculateBoundsOptions: { + forceNow: nowProvider.get(), }, - }; + }); return { calculateAutoTimeExpression, - createAggConfigs: (indexPattern, configStates = []) => { - return new AggConfigs( - indexPattern, - configStates, - { typesRegistry, aggExecutionContext }, - this.getConfig! - ); - }, - types: typesRegistry, + createAggConfigs, + types, }; } diff --git a/src/plugins/data/public/search/expressions/esaggs.test.ts b/src/plugins/data/public/search/expressions/esaggs.test.ts index 0c5a24adf6e14..412b018d54ef5 100644 --- a/src/plugins/data/public/search/expressions/esaggs.test.ts +++ b/src/plugins/data/public/search/expressions/esaggs.test.ts @@ -87,14 +87,18 @@ describe('esaggs expression function - public', () => { expect(startDependencies.aggs.createAggConfigs).toHaveBeenCalledWith( {}, - args.aggs.map((agg) => agg.value) + args.aggs.map((agg) => agg.value), + { hierarchical: true, partialRows: false } ); }); test('calls aggs.createAggConfigs with the empty aggs array when not provided', async () => { await definition().fn(null, omit(args, 'aggs'), mockHandlers).toPromise(); - expect(startDependencies.aggs.createAggConfigs).toHaveBeenCalledWith({}, []); + expect(startDependencies.aggs.createAggConfigs).toHaveBeenCalledWith({}, [], { + hierarchical: true, + partialRows: false, + }); }); test('calls getEsaggsMeta to retrieve meta', () => { @@ -111,8 +115,6 @@ describe('esaggs expression function - public', () => { abortSignal: mockHandlers.abortSignal, aggs: { foo: 'bar', - hierarchical: true, - partialRows: args.partialRows, }, filters: undefined, indexPattern: {}, diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index 342abc854138e..7ea785d0df457 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -43,10 +43,9 @@ export function getFunctionDefinition({ const indexPattern = await indexPatterns.create(args.index.value, true); const aggConfigs = aggs.createAggConfigs( indexPattern, - args.aggs?.map((agg) => agg.value) ?? [] + args.aggs?.map((agg) => agg.value) ?? [], + { hierarchical: args.metricsAtAllLevels, partialRows: args.partialRows } ); - aggConfigs.hierarchical = args.metricsAtAllLevels; - aggConfigs.partialRows = args.partialRows; const { handleEsaggsRequest } = await import('../../../common/search/expressions'); diff --git a/src/plugins/data/public/search/search_service.test.ts b/src/plugins/data/public/search/search_service.test.ts index c8a7c3c9024b5..9dfc23db4eb8d 100644 --- a/src/plugins/data/public/search/search_service.test.ts +++ b/src/plugins/data/public/search/search_service.test.ts @@ -60,6 +60,9 @@ describe('Search service', () => { fieldFormats: {}, indexPatterns: {}, screenshotMode: screenshotModePluginMock.createStartContract(), + nowProvider: { + get: jest.fn(), + }, } as any); expect(start).toHaveProperty('aggs'); expect(start).toHaveProperty('search'); diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 6c7ec3af0f0d7..a945e3d8a5479 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -17,6 +17,7 @@ import { BehaviorSubject } from 'rxjs'; import React from 'react'; import moment from 'moment'; import { BfetchPublicSetup } from '@kbn/bfetch-plugin/public'; +import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import { ExpressionsSetup } from '@kbn/expressions-plugin/public'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; @@ -55,7 +56,7 @@ import { selectFilterFunction, eqlRawResponse, } from '../../common/search'; -import { AggsService, AggsStartDependencies } from './aggs'; +import { AggsService } from './aggs'; import { IKibanaSearchResponse, SearchRequest } from '..'; import { ISearchInterceptor, SearchInterceptor } from './search_interceptor'; import { createUsageCollector, SearchUsageCollector } from './collectors'; @@ -79,15 +80,16 @@ export interface SearchServiceSetupDependencies { bfetch: BfetchPublicSetup; expressions: ExpressionsSetup; usageCollection?: UsageCollectionSetup; - nowProvider: NowProviderInternalContract; management: ManagementSetup; + nowProvider: NowProviderInternalContract; } /** @internal */ export interface SearchServiceStartDependencies { - fieldFormats: AggsStartDependencies['fieldFormats']; + fieldFormats: FieldFormatsStart; indexPatterns: DataViewsContract; screenshotMode: ScreenshotModePluginStart; + nowProvider: NowProviderInternalContract; } export class SearchService implements Plugin { @@ -187,9 +189,8 @@ export class SearchService implements Plugin { expressions.registerType(eqlRawResponse); const aggs = this.aggsService.setup({ - registerFunction: expressions.registerFunction, uiSettings, - nowProvider, + registerFunction: expressions.registerFunction, }); if (this.initializerContext.config.get().search.aggs.shardDelay.enabled) { @@ -222,7 +223,7 @@ export class SearchService implements Plugin { public start( { http, theme, uiSettings, chrome, application }: CoreStart, - { fieldFormats, indexPatterns, screenshotMode }: SearchServiceStartDependencies + { fieldFormats, indexPatterns, screenshotMode, nowProvider }: SearchServiceStartDependencies ): ISearchStart { const search = ((request, options = {}) => { return this.searchInterceptor.search(request, options); @@ -231,7 +232,8 @@ export class SearchService implements Plugin { const loadingCount$ = new BehaviorSubject(0); http.addLoadingCountSource(loadingCount$); - const aggs = this.aggsService.start({ fieldFormats, uiSettings, indexPatterns }); + const aggs = this.aggsService.start({ fieldFormats, indexPatterns, nowProvider }); + const searchSourceDependencies: SearchSourceDependencies = { aggs, getConfig: uiSettings.get.bind(uiSettings), diff --git a/src/plugins/data/server/search/aggs/aggs_service.ts b/src/plugins/data/server/search/aggs/aggs_service.ts index beaabb044511b..57f9ccebee23c 100644 --- a/src/plugins/data/server/search/aggs/aggs_service.ts +++ b/src/plugins/data/server/search/aggs/aggs_service.ts @@ -8,7 +8,8 @@ import { pick } from 'lodash'; -import { +import type { + IUiSettingsClient, UiSettingsServiceStart, SavedObjectsClientContract, ElasticsearchClient, @@ -16,14 +17,7 @@ import { import { ExpressionsServiceSetup } from '@kbn/expressions-plugin/common'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/server'; import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; -import { - AggConfigs, - AggsCommonService, - AggTypesDependencies, - aggsRequiredUiSettings, - calculateBounds, - TimeRange, -} from '../../../common'; +import { AggsCommonService, aggsRequiredUiSettings } from '../../../common'; import { AggsSetup, AggsStart } from './types'; /** @internal */ @@ -38,22 +32,26 @@ export interface AggsStartDependencies { indexPatterns: DataViewsServerPluginStart; } +async function getConfigFn(uiSettingsClient: IUiSettingsClient) { + // cache ui settings, only including items which are explicitly needed by aggs + const uiSettingsCache = pick(await uiSettingsClient.getAll(), aggsRequiredUiSettings); + return (key: string): T => { + return uiSettingsCache[key]; + }; +} + /** * The aggs service provides a means of modeling and manipulating the various * Elasticsearch aggregations supported by Kibana, providing the ability to * output the correct DSL when you are ready to send your request to ES. */ export class AggsService { - private readonly aggsCommonService = new AggsCommonService(); - - /** - * getForceNow uses window.location on the client, so we must have a - * separate implementation of calculateBounds on the server. - */ - private calculateBounds = (timeRange: TimeRange) => calculateBounds(timeRange, {}); + private readonly aggsCommonService = new AggsCommonService({ shouldDetectTimeZone: false }); public setup({ registerFunction }: AggsSetupDependencies): AggsSetup { - return this.aggsCommonService.setup({ registerFunction }); + return this.aggsCommonService.setup({ + registerFunction, + }); } public start({ fieldFormats, uiSettings, indexPatterns }: AggsStartDependencies): AggsStart { @@ -63,63 +61,20 @@ export class AggsService { elasticsearchClient: ElasticsearchClient ) => { const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); - const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient); - - // cache ui settings, only including items which are explicitly needed by aggs - const uiSettingsCache = pick(await uiSettingsClient.getAll(), aggsRequiredUiSettings); - const getConfig = (key: string): T => { - return uiSettingsCache[key]; - }; - - const aggExecutionContext: AggTypesDependencies['aggExecutionContext'] = { - shouldDetectTimeZone: false, - }; - - const { calculateAutoTimeExpression, types } = this.aggsCommonService.start({ - getConfig, - aggExecutionContext, - getIndexPattern: ( - await indexPatterns.dataViewsServiceFactory(savedObjectsClient, elasticsearchClient) - ).get, - }); - - const aggTypesDependencies: AggTypesDependencies = { - calculateBounds: this.calculateBounds, - aggExecutionContext, - getConfig, - getFieldFormatsStart: () => ({ - deserialize: formats.deserialize, - getDefaultInstance: formats.getDefaultInstance, - }), - }; - - const typesRegistry = { - get: (name: string) => { - const type = types.get(name); - if (!type) { - return; - } - return type(aggTypesDependencies); - }, - getAll: () => { - return { - // initialize each agg type on the fly - buckets: types.getAll().buckets.map((type) => type(aggTypesDependencies)), - metrics: types.getAll().metrics.map((type) => type(aggTypesDependencies)), - }; - }, - }; + const { calculateAutoTimeExpression, types, createAggConfigs } = + this.aggsCommonService.start({ + getConfig: await getConfigFn(uiSettingsClient), + fieldFormats: await fieldFormats.fieldFormatServiceFactory(uiSettingsClient), + calculateBoundsOptions: {}, + getIndexPattern: ( + await indexPatterns.dataViewsServiceFactory(savedObjectsClient, elasticsearchClient) + ).get, + }); return { calculateAutoTimeExpression, - createAggConfigs: (indexPattern, configStates = []) => - new AggConfigs( - indexPattern, - configStates, - { typesRegistry, aggExecutionContext }, - getConfig - ), - types: typesRegistry, + createAggConfigs, + types, }; }, }; diff --git a/src/plugins/data/server/search/aggs/types.ts b/src/plugins/data/server/search/aggs/types.ts index d931f4fb361f2..416bcdbb81d31 100644 --- a/src/plugins/data/server/search/aggs/types.ts +++ b/src/plugins/data/server/search/aggs/types.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { SavedObjectsClientContract, ElasticsearchClient } from '@kbn/core/server'; -import { AggsCommonSetup, AggsStart as Start } from '../../../common'; +import type { SavedObjectsClientContract, ElasticsearchClient } from '@kbn/core/server'; +import type { AggsCommonSetup, AggsStart as Start } from '../../../common'; export type AggsSetup = AggsCommonSetup; diff --git a/src/plugins/data/server/search/expressions/esaggs.test.ts b/src/plugins/data/server/search/expressions/esaggs.test.ts index 8f7ec47e18f47..6b6daf1da4a57 100644 --- a/src/plugins/data/server/search/expressions/esaggs.test.ts +++ b/src/plugins/data/server/search/expressions/esaggs.test.ts @@ -95,14 +95,18 @@ describe('esaggs expression function - server', () => { expect(startDependencies.aggs.createAggConfigs).toHaveBeenCalledWith( {}, - args.aggs.map((agg) => agg.value) + args.aggs.map((agg) => agg.value), + { hierarchical: true, partialRows: false } ); }); test('calls aggs.createAggConfigs with the empty aggs array when not provided', async () => { await definition().fn(null, omit(args, 'aggs'), mockHandlers).toPromise(); - expect(startDependencies.aggs.createAggConfigs).toHaveBeenCalledWith({}, []); + expect(startDependencies.aggs.createAggConfigs).toHaveBeenCalledWith({}, [], { + hierarchical: true, + partialRows: false, + }); }); test('calls getEsaggsMeta to retrieve meta', () => { @@ -119,8 +123,6 @@ describe('esaggs expression function - server', () => { abortSignal: mockHandlers.abortSignal, aggs: { foo: 'bar', - hierarchical: args.metricsAtAllLevels, - partialRows: args.partialRows, }, filters: undefined, indexPattern: {}, diff --git a/src/plugins/data/server/search/expressions/esaggs.ts b/src/plugins/data/server/search/expressions/esaggs.ts index bca2ac63b7f0f..5b303f071f01f 100644 --- a/src/plugins/data/server/search/expressions/esaggs.ts +++ b/src/plugins/data/server/search/expressions/esaggs.ts @@ -56,12 +56,10 @@ export function getFunctionDefinition({ const indexPattern = await indexPatterns.create(args.index.value, true); const aggConfigs = aggs.createAggConfigs( indexPattern, - args.aggs?.map((agg) => agg.value) ?? [] + args.aggs?.map((agg) => agg.value) ?? [], + { hierarchical: args.metricsAtAllLevels, partialRows: args.partialRows } ); - aggConfigs.hierarchical = args.metricsAtAllLevels; - aggConfigs.partialRows = args.partialRows; - return { aggConfigs, indexPattern, searchSource }; }).pipe( switchMap(({ aggConfigs, indexPattern, searchSource }) => diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 6561e6e127d0b..b5d140c36a0f5 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -248,7 +248,9 @@ export class SearchService implements Plugin { expressions.registerType(esRawResponse); expressions.registerType(eqlRawResponse); - const aggs = this.aggsService.setup({ registerFunction: expressions.registerFunction }); + const aggs = this.aggsService.setup({ + registerFunction: expressions.registerFunction, + }); firstValueFrom(this.initializerContext.config.create()).then((value) => { if (value.search.aggs.shardDelay.enabled) { diff --git a/src/plugins/visualizations/public/embeddable/to_ast.ts b/src/plugins/visualizations/public/embeddable/to_ast.ts index 80e7217f8d1c1..686c1bf7b3271 100644 --- a/src/plugins/visualizations/public/embeddable/to_ast.ts +++ b/src/plugins/visualizations/public/embeddable/to_ast.ts @@ -6,10 +6,10 @@ * Side Public License, v 1. */ -import { ExpressionFunctionKibana } from '@kbn/data-plugin/public'; +import type { ExpressionFunctionKibana } from '@kbn/data-plugin/public'; import { ExpressionAstExpression, buildExpressionFunction } from '@kbn/expressions-plugin/public'; -import { VisToExpressionAst } from '../types'; +import type { VisToExpressionAst } from '../types'; /** * Creates an ast expression for a visualization based on kibana context (query, filters, timerange) @@ -29,12 +29,17 @@ export const toExpressionAst: VisToExpressionAst = async ( const searchSource = vis.data.searchSource?.createCopy(); if (vis.data.aggs) { - vis.data.aggs.hierarchical = vis.isHierarchical(); - vis.data.aggs.partialRows = - typeof vis.type.hasPartialRows === 'function' - ? vis.type.hasPartialRows(vis) - : vis.type.hasPartialRows; - searchSource?.setField('aggs', vis.data.aggs); + const aggs = vis.data.aggs.clone({ + opts: { + hierarchical: vis.isHierarchical(), + partialRows: + typeof vis.type.hasPartialRows === 'function' + ? vis.type.hasPartialRows(vis) + : vis.type.hasPartialRows, + }, + }); + + searchSource?.setField('aggs', aggs); } const visExpressionAst = await vis.type.toExpressionAst(vis, params); From 707c034fd26887ef39d9ccaf964006bafc721708 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Mon, 13 Jun 2022 09:44:17 -0400 Subject: [PATCH 27/62] synthetics - skip hydration when the user does not have permissions (#133913) Co-authored-by: Shahzad --- .../hydrate_saved_object.test.ts | 60 +++++++++++++++++-- .../hydrate_saved_object.ts | 23 ++++++- 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/synthetics/server/synthetics_service/hydrate_saved_object.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/hydrate_saved_object.test.ts index 26e66a8b09e7f..86efbd22df56c 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/hydrate_saved_object.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/hydrate_saved_object.test.ts @@ -8,12 +8,13 @@ import { hydrateSavedObjects } from './hydrate_saved_object'; import { DecryptedSyntheticsMonitorSavedObject } from '../../common/types'; import { UptimeServerSetup } from '../legacy_uptime/lib/adapters'; +import { getUptimeESMockClient } from '../legacy_uptime/lib/requests/helper'; +import { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; + import moment from 'moment'; describe('hydrateSavedObjects', () => { - const mockEsClient = { - search: jest.fn(), - }; + const { uptimeEsClient: mockUptimeEsClient, esClient: mockEsClient } = getUptimeESMockClient(); const mockMonitorTemplate = { id: 'my-mock-monitor', @@ -24,7 +25,7 @@ describe('hydrateSavedObjects', () => { }; const serverMock: UptimeServerSetup = { - uptimeEsClient: mockEsClient, + uptimeEsClient: mockUptimeEsClient, authSavedObjectsClient: { bulkUpdate: jest.fn(), }, @@ -34,6 +35,12 @@ describe('hydrateSavedObjects', () => { body: { hits: { hits } }, }); + beforeEach(() => { + mockUptimeEsClient.baseESClient.security.hasPrivileges = jest + .fn() + .mockResolvedValue({ has_all_requested: true }); + }); + it.each([['browser'], ['http'], ['tcp']])( 'hydrates missing data for %s monitors', async (type) => { @@ -55,7 +62,7 @@ describe('hydrateSavedObjects', () => { url: { port: 443, full: 'https://example.com' }, }, }, - ]) + ]) as unknown as SearchResponse ); await hydrateSavedObjects({ monitors, server: serverMock }); @@ -72,4 +79,47 @@ describe('hydrateSavedObjects', () => { ]); } ); + + it.each([['browser'], ['http'], ['tcp']])( + 'does not hydrate when the user does not have permissions', + async (type) => { + const time = moment(); + const monitor = { + ...mockMonitorTemplate, + attributes: { ...mockMonitorTemplate.attributes, type }, + updated_at: moment(time).subtract(1, 'hour').toISOString(), + } as DecryptedSyntheticsMonitorSavedObject; + + const monitors: DecryptedSyntheticsMonitorSavedObject[] = [monitor]; + + mockUptimeEsClient.baseESClient.security.hasPrivileges = jest + .fn() + .mockResolvedValue({ has_all_requested: false }); + + mockEsClient.search.mockResolvedValue( + toKibanaResponse([ + { + _source: { + config_id: monitor.id, + '@timestamp': moment(time).toISOString(), + url: { port: 443, full: 'https://example.com' }, + }, + }, + ]) as unknown as SearchResponse + ); + + await hydrateSavedObjects({ monitors, server: serverMock }); + + expect(serverMock.authSavedObjectsClient?.bulkUpdate).not.toHaveBeenCalledWith([ + { + ...monitor, + attributes: { + ...monitor.attributes, + 'url.port': 443, + urls: 'https://example.com', + }, + }, + ]); + } + ); }); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/hydrate_saved_object.ts b/x-pack/plugins/synthetics/server/synthetics_service/hydrate_saved_object.ts index 6a6f67979ad75..92f457d3c0516 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/hydrate_saved_object.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/hydrate_saved_object.ts @@ -6,6 +6,7 @@ */ import moment from 'moment'; +import type { SecurityIndexPrivilege } from '@elastic/elasticsearch/lib/api/types'; import { UptimeESClient } from '../legacy_uptime/lib/lib'; import { UptimeServerSetup } from '../legacy_uptime/lib/adapters'; import { DecryptedSyntheticsMonitorSavedObject } from '../../common/types'; @@ -20,6 +21,27 @@ export const hydrateSavedObjects = async ({ server: UptimeServerSetup; }) => { try { + const { uptimeEsClient } = server; + if (!uptimeEsClient) { + return; + } + + const { has_all_requested: hasAllPrivileges } = + await uptimeEsClient.baseESClient.security.hasPrivileges({ + body: { + index: [ + { + names: ['synthetics-*'], + privileges: ['read'] as SecurityIndexPrivilege[], + }, + ], + }, + }); + + if (!hasAllPrivileges) { + return; + } + const missingInfoIds: string[] = monitors .filter((monitor) => { const isBrowserMonitor = monitor.attributes.type === 'browser'; @@ -128,7 +150,6 @@ const fetchSampleMonitorDocuments = async (esClient: UptimeESClient, configIds: 'getHydrateQuery', SYNTHETICS_INDEX_PATTERN ); - return data.body.hits.hits.map( ({ _source: doc }) => ({ ...(doc as any), timestamp: (doc as any)['@timestamp'] } as Ping) ); From c129656d557013fcceaad7a9c7427bdaa6d40b15 Mon Sep 17 00:00:00 2001 From: Mark Hopkin Date: Mon, 13 Jun 2022 14:51:44 +0100 Subject: [PATCH 28/62] make template parts copyable (#134060) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../index_templates/simulate_template/simulate_template.tsx | 2 +- .../shared/components/details_panel/tab_aliases.tsx | 4 +++- .../shared/components/details_panel/tab_mappings.tsx | 4 +++- .../shared/components/details_panel/tab_settings.tsx | 4 +++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/index_management/public/application/components/index_templates/simulate_template/simulate_template.tsx b/x-pack/plugins/index_management/public/application/components/index_templates/simulate_template/simulate_template.tsx index 8379f266108c5..325ef03227826 100644 --- a/x-pack/plugins/index_management/public/application/components/index_templates/simulate_template/simulate_template.tsx +++ b/x-pack/plugins/index_management/public/application/components/index_templates/simulate_template/simulate_template.tsx @@ -90,7 +90,7 @@ export const SimulateTemplate = React.memo(({ template, filters }: Props) => { } return isEmpty ? null : ( - + {templatePreview} ); diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_aliases.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_aliases.tsx index 269f4265ecff3..c02404596dcc5 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_aliases.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_aliases.tsx @@ -19,7 +19,9 @@ export const TabAliases: React.FunctionComponent = ({ aliases }) => { if (aliases && Object.keys(aliases).length) { return (
    - {JSON.stringify(aliases, null, 2)} + + {JSON.stringify(aliases, null, 2)} +
    ); } diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_mappings.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_mappings.tsx index 05d1f0d8f6aeb..0ed22589f8f3d 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_mappings.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_mappings.tsx @@ -18,7 +18,9 @@ export const TabMappings: React.FunctionComponent = ({ mappings }) => { if (mappings && Object.keys(mappings).length) { return (
    - {JSON.stringify(mappings, null, 2)} + + {JSON.stringify(mappings, null, 2)} +
    ); } diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_settings.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_settings.tsx index 8f749af20b61b..915adb6cf849a 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_settings.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_settings.tsx @@ -18,7 +18,9 @@ export const TabSettings: React.FunctionComponent = ({ settings }) => { if (settings && Object.keys(settings).length) { return (
    - {JSON.stringify(settings, null, 2)} + + {JSON.stringify(settings, null, 2)} +
    ); } From 439a56d8f7048dcb37bfd0961d054383c2d6785f Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 13 Jun 2022 15:57:23 +0200 Subject: [PATCH 29/62] fix voiceover drag and drop (#134196) --- x-pack/plugins/lens/public/drag_drop/drag_drop.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx index 143edddd8b7b3..d8a526380cd6d 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx @@ -274,6 +274,7 @@ const DragInner = memo(function DragInner({ ); const modifierHandlers = useMemo(() => { const onKeyUp = (e: KeyboardEvent) => { + e.preventDefault(); if (activeDropTarget?.id && ['Shift', 'Alt', 'Control'].includes(e.key)) { if (e.altKey) { setTargetOfIndex(activeDropTarget.id, 1); @@ -292,6 +293,7 @@ const DragInner = memo(function DragInner({ } }; const onKeyDown = (e: KeyboardEvent) => { + e.preventDefault(); if (e.key === 'Alt' && activeDropTarget?.id) { setTargetOfIndex(activeDropTarget.id, 1); } else if (e.key === 'Shift' && activeDropTarget?.id) { @@ -410,7 +412,7 @@ const DragInner = memo(function DragInner({ aria-describedby={ariaDescribedBy || `lnsDragDrop-keyboardInstructions`} className="lnsDragDrop__keyboardHandler" data-test-subj="lnsDragDrop-keyboardHandler" - onBlur={() => { + onBlur={(e) => { if (activeDraggingProps) { dragEnd(); } From 4e5f93023331c0461a223f9b649271f4e9e761d4 Mon Sep 17 00:00:00 2001 From: Katerina Patticha Date: Mon, 13 Jun 2022 15:57:37 +0200 Subject: [PATCH 30/62] Fix x-axis on chart error (#134193) --- .../apm/server/routes/errors/distribution/get_distribution.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/apm/server/routes/errors/distribution/get_distribution.ts b/x-pack/plugins/apm/server/routes/errors/distribution/get_distribution.ts index b08113be712c2..f4df8ab5b7dfb 100644 --- a/x-pack/plugins/apm/server/routes/errors/distribution/get_distribution.ts +++ b/x-pack/plugins/apm/server/routes/errors/distribution/get_distribution.ts @@ -55,8 +55,8 @@ export async function getErrorDistribution({ }; const currentPeriodPromise = getBuckets({ ...commonProps, - start: startWithOffset, - end: endWithOffset, + start, + end, }); const previousPeriodPromise = offset From 90e46180fe84fee89c0a929ce35314eecedb3469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=BCrkan=20YALAMAN?= Date: Mon, 13 Jun 2022 16:00:49 +0200 Subject: [PATCH 31/62] [Enterprise Search] Reintroduce content plugin for next release (#134026) * Reintroduce content plugin for next release Adds simple tabs on the overview tab * Updated comments to be accurate as suggested on review Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../collectors/application_usage/schema.ts | 1 + src/plugins/telemetry/schema/oss_plugins.json | 131 ++++++++++++++++++ .../components/search_index/overview.tsx | 79 ++++++++++- .../components/search_index/total_stats.tsx | 46 ++++++ .../enterprise_search/public/plugin.ts | 24 ++++ .../enterprise_search/server/plugin.ts | 2 + .../security_and_spaces/tests/catalogue.ts | 1 + .../security_and_spaces/tests/nav_links.ts | 1 + .../spaces_only/tests/catalogue.ts | 3 +- .../spaces_only/tests/nav_links.ts | 3 +- 10 files changed, 287 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/total_stats.tsx diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts index bd21e700c5990..e650dc5bbc3c4 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts @@ -134,6 +134,7 @@ export const applicationUsageSchema = { apm: commonSchema, canvas: commonSchema, enterpriseSearch: commonSchema, + enterpriseSearchContent: commonSchema, elasticsearch: commonSchema, appSearch: commonSchema, workplaceSearch: commonSchema, diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 19ebcfbe0fef9..f95e75e528d80 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -2223,6 +2223,137 @@ } } }, + "enterpriseSearchContent": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "Always `main`" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 90 days" + } + }, + "views": { + "type": "array", + "items": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "The application view being tracked" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application sub view since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application sub view is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 90 days" + } + } + } + } + } + } + }, "elasticsearch": { "properties": { "appId": { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/overview.tsx index 52ee63cc11104..797624baaccfe 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/overview.tsx @@ -5,18 +5,93 @@ * 2.0. */ -import React from 'react'; +import React, { useState } from 'react'; + +import { + EuiCodeBlock, + EuiText, + EuiFlexGroup, + EuiButton, + EuiButtonIcon, + EuiFlexItem, + EuiPanel, + EuiTabs, + EuiTab, + EuiSpacer, +} from '@elastic/eui'; + +import { getEnterpriseSearchUrl } from '../../../shared/enterprise_search_url/external_url'; import { EnterpriseSearchContentPageTemplate } from '../layout/page_template'; +import { DOCUMENTS_API_JSON_EXAMPLE } from '../new_index/constants'; + +import { TotalStats } from './total_stats'; export const SearchIndexOverview: React.FC = () => { + const [tabIndex, setSelectedTabIndex] = useState(0); + const searchIndexApiUrl = getEnterpriseSearchUrl('/api/ent/v1/search_indices/'); + const apiKey = 'Create an API Key'; + return ( - <>Overview + + setSelectedTabIndex(0)}> + Overview + + setSelectedTabIndex(1)}> + Document explorer + + setSelectedTabIndex(2)}> + Index mappings + + setSelectedTabIndex(3)}> + Configuration + + + + + + + + + + + + +

    Indexing by API

    +
    +
    + + + + + + + Generate an API key + + + +
    +
    + + + {`\ +curl -X POST '${searchIndexApiUrl}${name}/document' \\ + -H 'Content-Type: application/json' \\ + -H 'Authorization: Bearer ${apiKey}' \\ + -d '${JSON.stringify(DOCUMENTS_API_JSON_EXAMPLE, null, 2)}' +`} + + +
    +
    +
    +
    ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/total_stats.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/total_stats.tsx new file mode 100644 index 0000000000000..411f8ba4c285f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/total_stats.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiStat } from '@elastic/eui'; + +export const TotalStats: React.FC = () => { + const [{ lastUpdated, documentCount, indexHealth, ingestionType }] = useState({ + lastUpdated: 'Just now', + documentCount: 0, + indexHealth: 'Healthy', + ingestionType: 'API', + }); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index ee76180f6da54..9d84b165cfbae 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -24,6 +24,7 @@ import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/p import { APP_SEARCH_PLUGIN, ELASTICSEARCH_PLUGIN, + ENTERPRISE_SEARCH_CONTENT_PLUGIN, ENTERPRISE_SEARCH_OVERVIEW_PLUGIN, WORKPLACE_SEARCH_PLUGIN, } from '../common/constants'; @@ -89,6 +90,29 @@ export class EnterpriseSearchPlugin implements Plugin { }, }); + core.application.register({ + id: ENTERPRISE_SEARCH_CONTENT_PLUGIN.ID, + title: ENTERPRISE_SEARCH_CONTENT_PLUGIN.NAV_TITLE, + euiIconType: ENTERPRISE_SEARCH_CONTENT_PLUGIN.LOGO, + appRoute: ENTERPRISE_SEARCH_CONTENT_PLUGIN.URL, + category: DEFAULT_APP_CATEGORIES.enterpriseSearch, + mount: async (params: AppMountParameters) => { + const kibanaDeps = await this.getKibanaDeps(core, params, cloud); + const { chrome, http } = kibanaDeps.core; + chrome.docTitle.change(ENTERPRISE_SEARCH_CONTENT_PLUGIN.NAME); + + await this.getInitialData(http); + const pluginData = this.getPluginData(); + + const { renderApp } = await import('./applications'); + const { EnterpriseSearchContent } = await import( + './applications/enterprise_search_content' + ); + + return renderApp(EnterpriseSearchContent, kibanaDeps, pluginData); + }, + }); + core.application.register({ id: ELASTICSEARCH_PLUGIN.ID, title: ELASTICSEARCH_PLUGIN.NAME, diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index 89e849127cd24..c3dbfd99359de 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -24,6 +24,7 @@ import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import { ENTERPRISE_SEARCH_OVERVIEW_PLUGIN, + ENTERPRISE_SEARCH_CONTENT_PLUGIN, ELASTICSEARCH_PLUGIN, APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN, @@ -92,6 +93,7 @@ export class EnterpriseSearchPlugin implements Plugin { const log = this.logger; const PLUGIN_IDS = [ ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.ID, + ENTERPRISE_SEARCH_CONTENT_PLUGIN.ID, ELASTICSEARCH_PLUGIN.ID, APP_SEARCH_PLUGIN.ID, WORKPLACE_SEARCH_PLUGIN.ID, diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts index be61b9cf5e5a6..40ae1d5624776 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts @@ -63,6 +63,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) { const exceptions = [ 'monitoring', 'enterpriseSearch', + 'enterpriseSearchContent', 'elasticsearch', 'appSearch', 'workplaceSearch', diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts index 10e5cc1c37cde..74f1150965c5e 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts @@ -53,6 +53,7 @@ export default function navLinksTests({ getService }: FtrProviderContext) { navLinksBuilder.except( 'monitoring', 'enterpriseSearch', + 'enterpriseSearchContent', 'appSearch', 'workplaceSearch' ) diff --git a/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts b/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts index 0e773845508da..770de61b5f797 100644 --- a/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts @@ -25,8 +25,9 @@ export default function catalogueTests({ getService }: FtrProviderContext) { ]; const uiCapabilitiesExceptions = [ - // enterprise_search plugin is loaded but disabled because security isn't enabled in ES. That means the following 4 capabilities are disabled + // enterprise_search plugin is loaded but disabled because security isn't enabled in ES. That means the following capabilities are disabled 'enterpriseSearch', + 'enterpriseSearchContent', 'elasticsearch', 'appSearch', 'workplaceSearch', diff --git a/x-pack/test/ui_capabilities/spaces_only/tests/nav_links.ts b/x-pack/test/ui_capabilities/spaces_only/tests/nav_links.ts index 222a0a7ae215e..0e0ebe202eb7d 100644 --- a/x-pack/test/ui_capabilities/spaces_only/tests/nav_links.ts +++ b/x-pack/test/ui_capabilities/spaces_only/tests/nav_links.ts @@ -17,8 +17,9 @@ export default function navLinksTests({ getService }: FtrProviderContext) { const featuresService: FeaturesService = getService('features'); const uiCapabilitiesExceptions = [ - // enterprise_search plugin is loaded but disabled because security isn't enabled in ES. That means the following 4 capabilities are disabled + // enterprise_search plugin is loaded but disabled because security isn't enabled in ES. That means the following capabilities are disabled 'enterpriseSearch', + 'enterpriseSearchContent', 'appSearch', 'workplaceSearch', ]; From 68e569bd7db4961138d5d2e95ef4d6b389698dbf Mon Sep 17 00:00:00 2001 From: John Dorlus Date: Mon, 13 Jun 2022 10:02:23 -0400 Subject: [PATCH 32/62] Fixed the query to make sure results contain the expected document. (#134154) --- test/functional/apps/console/_console_ccs.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/console/_console_ccs.ts b/test/functional/apps/console/_console_ccs.ts index 3ab81e076158b..f7c85f53c38d0 100644 --- a/test/functional/apps/console/_console_ccs.ts +++ b/test/functional/apps/console/_console_ccs.ts @@ -37,7 +37,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.console.clearTextArea(); }); it('it should be able to access remote data', async () => { - await PageObjects.console.enterRequest('\nGET ftr-remote:logstash-*/_search'); + await PageObjects.console.enterRequest( + '\nGET ftr-remote:logstash-*/_search\n {\n "query": {\n "bool": {\n "must": [\n {"match": {"extension" : "jpg"' + ); await PageObjects.console.clickPlay(); await retry.try(async () => { const actualResponse = await PageObjects.console.getResponse(); From d32d9f571cd198252d7a1a69632acc6e15787dca Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 13 Jun 2022 16:09:33 +0200 Subject: [PATCH 33/62] [Synthetics] Prevent imports from legacy_uptime (#134185) * commit * update usage * update type --- .eslintrc.js | 13 ++++++ .../getting_started/getting_started_page.tsx | 6 ++- .../apps/synthetics/hooks/use_url_params.ts | 42 ++++--------------- 3 files changed, 24 insertions(+), 37 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 036b2123ee254..f88b514b514a2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -958,6 +958,19 @@ module.exports = { }, }, + { + // disable imports from legacy uptime plugin + files: ['x-pack/plugins/synthetics/public/apps/synthetics/**/*.{js,mjs,ts,tsx}'], + rules: { + 'no-restricted-imports': [ + 'error', + { + patterns: ['**/legacy_uptime/*'], + }, + ], + }, + }, + /** * Fleet overrides */ diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.tsx index 3a5ad23e1a247..704b5d350b9e0 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.tsx @@ -13,8 +13,6 @@ import styled from 'styled-components'; import { useBreadcrumbs } from '../../hooks'; import { getServiceLocations } from '../../state'; import { SimpleMonitorForm } from './simple_monitor_form'; -import { MONITORING_OVERVIEW_LABEL } from '../../../../legacy_uptime/routes'; - export const GettingStartedPage = () => { const dispatch = useDispatch(); @@ -95,3 +93,7 @@ const SELECT_DIFFERENT_MONITOR = i18n.translate( const OR_LABEL = i18n.translate('xpack.synthetics.gettingStarted.orLabel', { defaultMessage: 'Or', }); + +const MONITORING_OVERVIEW_LABEL = i18n.translate('xpack.synthetics.overview.heading', { + defaultMessage: 'Monitors', +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_url_params.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_url_params.ts index ec31c711cffc7..45f5b6cea4918 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_url_params.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_url_params.ts @@ -5,17 +5,14 @@ * 2.0. */ -import { useCallback, useEffect } from 'react'; -import { stringify } from 'query-string'; +import { useCallback } from 'react'; +import { parse, stringify } from 'query-string'; import { useLocation, useHistory } from 'react-router-dom'; -import { useDispatch, useSelector } from 'react-redux'; import { SyntheticsUrlParams, getSupportedUrlParams } from '../utils/url_params'; -// TODO: Create the following imports for new Synthetics App -import { selectedFiltersSelector } from '../../../legacy_uptime/state/selectors'; -import { setSelectedFilters } from '../../../legacy_uptime/state/actions/selected_filters'; -import { getFiltersFromMap } from '../../../legacy_uptime/hooks/use_selected_filters'; -import { getParsedParams } from '../../../legacy_uptime/lib/helper/parse_search'; +function getParsedParams(search: string) { + return search ? parse(search[0] === '?' ? search.slice(1) : search, { sort: false }) : {}; +} export type GetUrlParams = () => SyntheticsUrlParams; export type UpdateUrlParams = (updatedParams: { @@ -30,29 +27,9 @@ export const useGetUrlParams: GetUrlParams = () => { return getSupportedUrlParams(getParsedParams(search)); }; -const getMapFromFilters = (value: any): Map | undefined => { - try { - return new Map(JSON.parse(value)); - } catch { - return undefined; - } -}; - export const useUrlParams: SyntheticsUrlParamsHook = () => { const { pathname, search } = useLocation(); const history = useHistory(); - const dispatch = useDispatch(); - const selectedFilters = useSelector(selectedFiltersSelector); - const { filters } = useGetUrlParams(); - - useEffect(() => { - if (selectedFilters === null) { - const filterMap = getMapFromFilters(filters); - if (filterMap) { - dispatch(setSelectedFilters(getFiltersFromMap(filterMap))); - } - } - }, [dispatch, filters, selectedFilters]); const updateUrlParams: UpdateUrlParams = useCallback( (updatedParams) => { @@ -69,6 +46,7 @@ export const useUrlParams: SyntheticsUrlParamsHook = () => { if (value === undefined || value === '') { return params; } + return { ...params, [key]: value, @@ -80,14 +58,8 @@ export const useUrlParams: SyntheticsUrlParamsHook = () => { if (search !== updatedSearch) { history.push({ pathname, search: updatedSearch }); } - const filterMap = getMapFromFilters(mergedParams.filters); - if (!filterMap) { - dispatch(setSelectedFilters(null)); - } else { - dispatch(setSelectedFilters(getFiltersFromMap(filterMap))); - } }, - [dispatch, history, pathname, search] + [history, pathname, search] ); return [useGetUrlParams, updateUrlParams]; From 6e0086df003dc085f4e8c2171d4f7b2d9cfa0f45 Mon Sep 17 00:00:00 2001 From: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com> Date: Mon, 13 Jun 2022 15:28:55 +0100 Subject: [PATCH 34/62] [RAM] adds bulkUpdatesSchedules method to Task Manager API (#132637) Addresses: https://github.com/elastic/kibana/issues/124850 ## Summary - Adds new method Task Manager API `bulkUpdateSchedules` - Adds calling `taskManager.bulkUpdateSchedules` in rulesClient.bulkEdit to update tasks if updated rules have `scheduleTaskId` property - Enables the rest of operations for rulesClient.bulkEdit (set schedule, notifyWhen, throttle) - #### bulkUpdateSchedules Using `bulkUpdatesSchedules` you can instruct TaskManager to update interval of tasks that are in `idle` status. When interval updated, new `runAt` will be computed and task will be updated with that value ```js export class Plugin { constructor() { } public setup(core: CoreSetup, plugins: { taskManager }) { } public start(core: CoreStart, plugins: { taskManager }) { try { const bulkUpdateResults = await taskManager.bulkUpdateSchedule( ['97c2c4e7-d850-11ec-bf95-895ffd19f959', 'a5ee24d1-dce2-11ec-ab8d-cf74da82133d'], { interval: '10m' }, ); // If no error is thrown, the bulkUpdateSchedule has completed successfully. // But some updates of some tasks can be failed, due to OCC 409 conflict for example } catch(err: Error) { // if error is caught, means the whole method requested has failed and tasks weren't updated } } } ``` ### in follow-up PRs - use `taskManager.bulkUpdateSchedules` in rulesClient.update (https://github.com/elastic/kibana/pull/134027) - functional test for bulkEdit (https://github.com/elastic/kibana/pull/133635) ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### Release note Adds new method to Task Manager - bulkUpdatesSchedules, that allow bulk updates of scheduled tasks. Adds 3 new operations to rulesClient.bulkUpdate: update of schedule, notifyWhen, throttle. --- .../alerting/server/routes/bulk_edit_rules.ts | 23 ++- .../server/rules_client/rules_client.ts | 67 ++++++--- .../rules_client/tests/bulk_edit.test.ts | 77 ++++++++++ x-pack/plugins/task_manager/README.md | 35 +++++ x-pack/plugins/task_manager/server/index.ts | 2 +- x-pack/plugins/task_manager/server/mocks.ts | 1 + x-pack/plugins/task_manager/server/plugin.ts | 3 +- .../server/task_scheduling.test.ts | 135 ++++++++++++++++++ .../task_manager/server/task_scheduling.ts | 90 +++++++++++- .../group1/tests/alerting/bulk_edit.ts | 21 +-- .../sample_task_plugin/server/init_routes.ts | 25 ++++ .../task_manager/task_management.ts | 107 +++++++++++++- 12 files changed, 548 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/alerting/server/routes/bulk_edit_rules.ts b/x-pack/plugins/alerting/server/routes/bulk_edit_rules.ts index 6588a46e1d914..42946027a555e 100644 --- a/x-pack/plugins/alerting/server/routes/bulk_edit_rules.ts +++ b/x-pack/plugins/alerting/server/routes/bulk_edit_rules.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '@kbn/core/server'; -import { ILicenseState, RuleTypeDisabledError } from '../lib'; +import { ILicenseState, RuleTypeDisabledError, validateDurationSchema } from '../lib'; import { verifyAccessAndContext, rewriteRule, handleDisabledApiKeysError } from './lib'; import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types'; @@ -34,6 +34,27 @@ const operationsSchema = schema.arrayOf( field: schema.literal('actions'), value: schema.arrayOf(ruleActionSchema), }), + schema.object({ + operation: schema.literal('set'), + field: schema.literal('schedule'), + value: schema.object({ interval: schema.string({ validate: validateDurationSchema }) }), + }), + schema.object({ + operation: schema.literal('set'), + field: schema.literal('throttle'), + value: schema.nullable(schema.string()), + }), + schema.object({ + operation: schema.literal('set'), + field: schema.literal('notifyWhen'), + value: schema.nullable( + schema.oneOf([ + schema.literal('onActionGroupChange'), + schema.literal('onActiveAlert'), + schema.literal('onThrottleInterval'), + ]) + ), + }), ]), { minSize: 1 } ); diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 017e1b5ffd937..fe61148b6350d 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -211,7 +211,10 @@ export interface FindOptions extends IndexType { filter?: string; } -export type BulkEditFields = keyof Pick; +export type BulkEditFields = keyof Pick< + Rule, + 'actions' | 'tags' | 'schedule' | 'throttle' | 'notifyWhen' +>; export type BulkEditOperation = | { @@ -223,25 +226,23 @@ export type BulkEditOperation = operation: 'add' | 'set'; field: Extract; value: NormalizedAlertAction[]; + } + | { + operation: 'set'; + field: Extract; + value: Rule['schedule']; + } + | { + operation: 'set'; + field: Extract; + value: Rule['throttle']; + } + | { + operation: 'set'; + field: Extract; + value: Rule['notifyWhen']; }; -// schedule, throttle, notifyWhen is commented out before https://github.com/elastic/kibana/issues/124850 will be implemented -// | { -// operation: 'set'; -// field: Extract; -// value: Rule['schedule']; -// } -// | { -// operation: 'set'; -// field: Extract; -// value: Rule['throttle']; -// } -// | { -// operation: 'set'; -// field: Extract; -// value: Rule['notifyWhen']; -// }; - type RuleParamsModifier = (params: Params) => Promise; export interface BulkEditOptionsFilter { @@ -1494,6 +1495,36 @@ export class RulesClient { ); }); + // update schedules only if schedule operation is present + const scheduleOperation = options.operations.find( + ( + operation + ): operation is Extract }> => + operation.field === 'schedule' + ); + + if (scheduleOperation?.value) { + const taskIds = updatedRules.reduce((acc, rule) => { + if (rule.scheduledTaskId) { + acc.push(rule.scheduledTaskId); + } + return acc; + }, []); + + try { + await this.taskManager.bulkUpdateSchedules(taskIds, scheduleOperation.value); + this.logger.debug( + `Successfully updated schedules for underlying tasks: ${taskIds.join(', ')}` + ); + } catch (error) { + this.logger.error( + `Failure to update schedules for underlying tasks: ${taskIds.join( + ', ' + )}. TaskManager bulkUpdateSchedules failed with Error: ${error.message}` + ); + } + } + return { rules: updatedRules, errors, total }; } diff --git a/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts index e878fd3f79e17..fe5f934ce4ab3 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts @@ -899,4 +899,81 @@ describe('bulkEdit()', () => { ); }); }); + + describe('task manager', () => { + test('should call task manager method bulkUpdateSchedules if operation set new schedules', async () => { + unsecuredSavedObjectsClient.bulkUpdate.mockResolvedValue({ + saved_objects: [ + { + id: '1', + type: 'alert', + attributes: { + enabled: true, + tags: ['foo'], + alertTypeId: 'myType', + schedule: { interval: '1m' }, + consumer: 'myApp', + scheduledTaskId: 'task-123', + params: { index: ['test-index-*'] }, + throttle: null, + notifyWhen: null, + actions: [], + }, + references: [], + version: '123', + }, + ], + }); + + await rulesClient.bulkEdit({ + operations: [ + { + field: 'schedule', + operation: 'set', + value: { interval: '10m' }, + }, + ], + }); + + expect(taskManager.bulkUpdateSchedules).toHaveBeenCalledWith(['task-123'], { + interval: '10m', + }); + }); + + test('should not call task manager method bulkUpdateSchedules if operation is not set schedule', async () => { + unsecuredSavedObjectsClient.bulkUpdate.mockResolvedValue({ + saved_objects: [ + { + id: '1', + type: 'alert', + attributes: { + enabled: true, + tags: ['foo'], + alertTypeId: 'myType', + schedule: { interval: '1m' }, + consumer: 'myApp', + params: { index: ['test-index-*'] }, + throttle: null, + notifyWhen: null, + actions: [], + }, + references: [], + version: '123', + }, + ], + }); + + await rulesClient.bulkEdit({ + operations: [ + { + field: 'tags', + operation: 'set', + value: ['test-tag'], + }, + ], + }); + + expect(taskManager.bulkUpdateSchedules).not.toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/plugins/task_manager/README.md b/x-pack/plugins/task_manager/README.md index 350a53c660bc7..a3f739d58e4eb 100644 --- a/x-pack/plugins/task_manager/README.md +++ b/x-pack/plugins/task_manager/README.md @@ -328,6 +328,9 @@ The _Start_ Plugin api allow you to use Task Manager to facilitate your Plugin's runNow: (taskId: string) => { // ... }, + bulkUpdateSchedules: (taskIds: string[], schedule: IntervalSchedule) => { + // ... + }, ensureScheduled: (taskInstance: TaskInstanceWithId, options?: any) => { // ... }, @@ -415,6 +418,38 @@ export class Plugin { } ``` +#### bulkUpdateSchedules +Using `bulkUpdatesSchedules` you can instruct TaskManger to update interval of tasks that are in `idle` status +(for the tasks which have `running` status, `schedule` and `runAt` will be recalculated after task run finishes). +When interval updated, new `runAt` will be computed and task will be updated with that value, using formula +``` +newRunAt = oldRunAt - oldInterval + newInterval +``` + +Example: +```js +export class Plugin { + constructor() { + } + + public setup(core: CoreSetup, plugins: { taskManager }) { + } + + public start(core: CoreStart, plugins: { taskManager }) { + try { + const bulkUpdateResults = await taskManager.bulkUpdateSchedule( + ['97c2c4e7-d850-11ec-bf95-895ffd19f959', 'a5ee24d1-dce2-11ec-ab8d-cf74da82133d'], + { interval: '10m' }, + ); + // If no error is thrown, the bulkUpdateSchedule has completed successfully. + // But some updates of some tasks can be failed, due to OCC 409 conflict for example + } catch(err: Error) { + // if error is caught, means the whole method requested has failed and tasks weren't updated + } + } +} +``` + #### more options More custom access to the tasks can be done directly via Elasticsearch, though that won't be officially supported, as we can change the document structure at any time. diff --git a/x-pack/plugins/task_manager/server/index.ts b/x-pack/plugins/task_manager/server/index.ts index f6cb3a6e6b1d5..88a27d040b176 100644 --- a/x-pack/plugins/task_manager/server/index.ts +++ b/x-pack/plugins/task_manager/server/index.ts @@ -30,7 +30,7 @@ export { throwUnrecoverableError, isEphemeralTaskRejectedDueToCapacityError, } from './task_running'; -export type { RunNowResult } from './task_scheduling'; +export type { RunNowResult, BulkUpdateSchedulesResult } from './task_scheduling'; export { getOldestIdleActionTask } from './queries/oldest_idle_action_task'; export { IdleTaskWithExpiredRunAt, diff --git a/x-pack/plugins/task_manager/server/mocks.ts b/x-pack/plugins/task_manager/server/mocks.ts index 2db8cdd6268c7..2870111ebafef 100644 --- a/x-pack/plugins/task_manager/server/mocks.ts +++ b/x-pack/plugins/task_manager/server/mocks.ts @@ -27,6 +27,7 @@ const createStartMock = () => { ensureScheduled: jest.fn(), removeIfExists: jest.fn(), supportsEphemeralTasks: jest.fn(), + bulkUpdateSchedules: jest.fn(), }; return mock; }; diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index 3694f01227178..0a7e25230b6d5 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -48,7 +48,7 @@ export interface TaskManagerSetupContract { export type TaskManagerStartContract = Pick< TaskScheduling, - 'schedule' | 'runNow' | 'ephemeralRunNow' | 'ensureScheduled' + 'schedule' | 'runNow' | 'ephemeralRunNow' | 'ensureScheduled' | 'bulkUpdateSchedules' > & Pick & { removeIfExists: TaskStore['remove']; @@ -238,6 +238,7 @@ export class TaskManagerPlugin schedule: (...args) => taskScheduling.schedule(...args), ensureScheduled: (...args) => taskScheduling.ensureScheduled(...args), runNow: (...args) => taskScheduling.runNow(...args), + bulkUpdateSchedules: (...args) => taskScheduling.bulkUpdateSchedules(...args), ephemeralRunNow: (task: EphemeralTask) => taskScheduling.ephemeralRunNow(task), supportsEphemeralTasks: () => this.config.ephemeral_tasks.enabled, }; diff --git a/x-pack/plugins/task_manager/server/task_scheduling.test.ts b/x-pack/plugins/task_manager/server/task_scheduling.test.ts index 6fe368d495ade..371266fc872ff 100644 --- a/x-pack/plugins/task_manager/server/task_scheduling.test.ts +++ b/x-pack/plugins/task_manager/server/task_scheduling.test.ts @@ -7,6 +7,7 @@ import { Subject } from 'rxjs'; import { none, some } from 'fp-ts/lib/Option'; +import moment from 'moment'; import { asTaskMarkRunningEvent, @@ -27,6 +28,7 @@ import { TaskRunResult } from './task_running'; import { mockLogger } from './test_utils'; import { TaskTypeDictionary } from './task_type_dictionary'; import { ephemeralTaskLifecycleMock } from './ephemeral_task_lifecycle.mock'; +import { mustBeAllOf } from './queries/query_clauses'; jest.mock('uuid', () => ({ v4: () => 'v4uuid', @@ -134,6 +136,139 @@ describe('TaskScheduling', () => { }); }); + describe('bulkUpdateSchedules', () => { + const id = '01ddff11-e88a-4d13-bc4e-256164e755e2'; + beforeEach(() => { + mockTaskStore.bulkUpdate.mockImplementation(() => + Promise.resolve([{ tag: 'ok', value: mockTask() }]) + ); + }); + + test('should search for tasks by ids and idle status', async () => { + mockTaskStore.fetch.mockResolvedValue({ docs: [] }); + const taskScheduling = new TaskScheduling(taskSchedulingOpts); + + await taskScheduling.bulkUpdateSchedules([id], { interval: '1h' }); + + expect(mockTaskStore.fetch).toHaveBeenCalledTimes(1); + expect(mockTaskStore.fetch).toHaveBeenCalledWith({ + query: mustBeAllOf( + { + terms: { + _id: [`task:${id}`], + }, + }, + { + term: { + 'task.status': 'idle', + }, + } + ), + size: 100, + }); + }); + + test('should split search on chunks when input ids array too large', async () => { + mockTaskStore.fetch.mockResolvedValue({ docs: [] }); + const taskScheduling = new TaskScheduling(taskSchedulingOpts); + + await taskScheduling.bulkUpdateSchedules(Array.from({ length: 1250 }), { interval: '1h' }); + + expect(mockTaskStore.fetch).toHaveBeenCalledTimes(13); + }); + + test('should transform response into correct format', async () => { + const successfulTask = mockTask({ id: 'task-1', schedule: { interval: '1h' } }); + const failedTask = mockTask({ id: 'task-2', schedule: { interval: '1h' } }); + mockTaskStore.bulkUpdate.mockImplementation(() => + Promise.resolve([ + { tag: 'ok', value: successfulTask }, + { tag: 'err', error: { entity: failedTask, error: new Error('fail') } }, + ]) + ); + mockTaskStore.fetch.mockResolvedValue({ docs: [successfulTask, failedTask] }); + + const taskScheduling = new TaskScheduling(taskSchedulingOpts); + const result = await taskScheduling.bulkUpdateSchedules([successfulTask.id, failedTask.id], { + interval: '1h', + }); + + expect(result).toEqual({ + tasks: [successfulTask], + errors: [{ task: failedTask, error: new Error('fail') }], + }); + }); + + test('should not update task if new interval is equal to previous', async () => { + const task = mockTask({ id, schedule: { interval: '3h' } }); + + mockTaskStore.fetch.mockResolvedValue({ docs: [task] }); + + const taskScheduling = new TaskScheduling(taskSchedulingOpts); + await taskScheduling.bulkUpdateSchedules([id], { interval: '3h' }); + + const bulkUpdatePayload = mockTaskStore.bulkUpdate.mock.calls[0][0]; + + expect(bulkUpdatePayload).toHaveLength(0); + }); + + test('should postpone task run if new interval is greater than previous', async () => { + // task set to be run in 2 hrs from now + const runInTwoHrs = new Date(Date.now() + moment.duration(2, 'hours').asMilliseconds()); + const task = mockTask({ id, schedule: { interval: '3h' }, runAt: runInTwoHrs }); + + mockTaskStore.fetch.mockResolvedValue({ docs: [task] }); + + const taskScheduling = new TaskScheduling(taskSchedulingOpts); + await taskScheduling.bulkUpdateSchedules([id], { interval: '5h' }); + + const bulkUpdatePayload = mockTaskStore.bulkUpdate.mock.calls[0][0]; + + expect(bulkUpdatePayload).toHaveLength(1); + expect(bulkUpdatePayload[0]).toHaveProperty('schedule', { interval: '5h' }); + // if tasks updated with schedule interval of '5h' and previous interval was 3h, task will be scheduled to run in 2 hours later + expect(bulkUpdatePayload[0].runAt.getTime() - runInTwoHrs.getTime()).toBe( + moment.duration(2, 'hours').asMilliseconds() + ); + }); + + test('should set task run sooner if new interval is lesser than previous', async () => { + // task set to be run in one 2hrs from now + const runInTwoHrs = new Date(Date.now() + moment.duration(2, 'hours').asMilliseconds()); + const task = mockTask({ id, schedule: { interval: '3h' }, runAt: runInTwoHrs }); + + mockTaskStore.fetch.mockResolvedValue({ docs: [task] }); + + const taskScheduling = new TaskScheduling(taskSchedulingOpts); + await taskScheduling.bulkUpdateSchedules([id], { interval: '2h' }); + + const bulkUpdatePayload = mockTaskStore.bulkUpdate.mock.calls[0][0]; + + expect(bulkUpdatePayload[0]).toHaveProperty('schedule', { interval: '2h' }); + // if tasks updated with schedule interval of '2h' and previous interval was 3h, task will be scheduled to run in 1 hour sooner + expect(runInTwoHrs.getTime() - bulkUpdatePayload[0].runAt.getTime()).toBe( + moment.duration(1, 'hour').asMilliseconds() + ); + }); + + test('should set task run to now if time that passed from last run is greater than new interval', async () => { + // task set to be run in one 1hr from now. With interval of '2h', it means last run happened 1 hour ago + const runInOneHr = new Date(Date.now() + moment.duration(1, 'hour').asMilliseconds()); + const task = mockTask({ id, schedule: { interval: '2h' }, runAt: runInOneHr }); + + mockTaskStore.fetch.mockResolvedValue({ docs: [task] }); + + const taskScheduling = new TaskScheduling(taskSchedulingOpts); + await taskScheduling.bulkUpdateSchedules([id], { interval: '30m' }); + + const bulkUpdatePayload = mockTaskStore.bulkUpdate.mock.calls[0][0]; + + expect(bulkUpdatePayload[0]).toHaveProperty('schedule', { interval: '30m' }); + + // if time that passed from last task run is greater than new interval, task should be set to run at now time + expect(bulkUpdatePayload[0].runAt.getTime()).toBeLessThanOrEqual(Date.now()); + }); + }); describe('runNow', () => { test('resolves when the task run succeeds', () => { const events$ = new Subject(); diff --git a/x-pack/plugins/task_manager/server/task_scheduling.ts b/x-pack/plugins/task_manager/server/task_scheduling.ts index a38e2d23fccec..31662ee8bce64 100644 --- a/x-pack/plugins/task_manager/server/task_scheduling.ts +++ b/x-pack/plugins/task_manager/server/task_scheduling.ts @@ -6,15 +6,16 @@ */ import { filter, take } from 'rxjs/operators'; - +import pMap from 'p-map'; import { pipe } from 'fp-ts/lib/pipeable'; import { Option, map as mapOptional, getOrElse, isSome } from 'fp-ts/lib/Option'; import uuid from 'uuid'; -import { pick } from 'lodash'; +import { pick, chunk } from 'lodash'; import { merge, Subject } from 'rxjs'; import agent from 'elastic-apm-node'; import { Logger } from '@kbn/core/server'; +import { mustBeAllOf } from './queries/query_clauses'; import { asOk, either, map, mapErr, promiseResult, isErr } from './lib/result_type'; import { isTaskRunEvent, @@ -28,6 +29,7 @@ import { TaskClaimErrorType, } from './task_events'; import { Middleware } from './lib/middleware'; +import { parseIntervalAsMillisecond } from './lib/intervals'; import { ConcreteTaskInstance, TaskInstanceWithId, @@ -36,6 +38,7 @@ import { TaskLifecycleResult, TaskStatus, EphemeralTask, + IntervalSchedule, } from './task'; import { TaskStore } from './task_store'; import { ensureDeprecatedFieldsAreCorrected } from './lib/correct_deprecated_fields'; @@ -56,6 +59,20 @@ export interface TaskSchedulingOpts { taskManagerId: string; } +/** + * return type of TaskScheduling.bulkUpdateSchedules method + */ +export interface BulkUpdateSchedulesResult { + /** + * list of successfully updated tasks + */ + tasks: ConcreteTaskInstance[]; + + /** + * list of failed tasks and errors caused failure + */ + errors: Array<{ task: ConcreteTaskInstance; error: Error }>; +} export interface RunNowResult { id: ConcreteTaskInstance['id']; state?: ConcreteTaskInstance['state']; @@ -111,6 +128,75 @@ export class TaskScheduling { }); } + /** + * Bulk updates schedules for tasks by ids. + * Only tasks with `idle` status will be updated, as for the tasks which have `running` status, + * `schedule` and `runAt` will be recalculated after task run finishes + * + * @param {string[]} taskIds - list of task ids + * @param {IntervalSchedule} schedule - new schedule + * @returns {Promise} + */ + public async bulkUpdateSchedules( + taskIds: string[], + schedule: IntervalSchedule + ): Promise { + const tasks = await pMap( + chunk(taskIds, 100), + async (taskIdsChunk) => + this.store.fetch({ + query: mustBeAllOf( + { + terms: { + _id: taskIdsChunk.map((taskId) => `task:${taskId}`), + }, + }, + { + term: { + 'task.status': 'idle', + }, + } + ), + size: 100, + }), + { concurrency: 10 } + ); + + const updatedTasks = tasks + .flatMap(({ docs }) => docs) + .reduce((acc, task) => { + // if task schedule interval is the same, no need to update it + if (task.schedule?.interval === schedule.interval) { + return acc; + } + + const oldIntervalInMs = parseIntervalAsMillisecond(task.schedule?.interval ?? '0s'); + + // computing new runAt using formula: + // newRunAt = oldRunAt - oldInterval + newInterval + const newRunAtInMs = Math.max( + Date.now(), + task.runAt.getTime() - oldIntervalInMs + parseIntervalAsMillisecond(schedule.interval) + ); + + acc.push({ ...task, schedule, runAt: new Date(newRunAtInMs) }); + return acc; + }, []); + + return (await this.store.bulkUpdate(updatedTasks)).reduce( + (acc, task) => { + if (task.tag === 'ok') { + acc.tasks.push(task.value); + } else { + acc.errors.push({ error: task.error.error, task: task.error.entity }); + } + + return acc; + }, + { tasks: [], errors: [] } + ); + } + /** * Run task. * diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_edit.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_edit.ts index 6eeafe8472499..6ae46cfd8d7bf 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_edit.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_edit.ts @@ -437,7 +437,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - '[request body.operations.0]: types that failed validation:\n- [request body.operations.0.0.operation]: types that failed validation:\n - [request body.operations.0.operation.0]: expected value to equal [add]\n - [request body.operations.0.operation.1]: expected value to equal [delete]\n - [request body.operations.0.operation.2]: expected value to equal [set]\n- [request body.operations.0.1.operation]: types that failed validation:\n - [request body.operations.0.operation.0]: expected value to equal [add]\n - [request body.operations.0.operation.1]: expected value to equal [set]', + '[request body.operations.0]: types that failed validation:\n- [request body.operations.0.0.operation]: types that failed validation:\n - [request body.operations.0.operation.0]: expected value to equal [add]\n - [request body.operations.0.operation.1]: expected value to equal [delete]\n - [request body.operations.0.operation.2]: expected value to equal [set]\n- [request body.operations.0.1.operation]: types that failed validation:\n - [request body.operations.0.operation.0]: expected value to equal [add]\n - [request body.operations.0.operation.1]: expected value to equal [set]\n- [request body.operations.0.2.operation]: expected value to equal [set]\n- [request body.operations.0.3.operation]: expected value to equal [set]\n- [request body.operations.0.4.operation]: expected value to equal [set]', }); expect(response.statusCode).to.eql(400); break; @@ -446,21 +446,14 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { } }); - it('should handle bulk edit of rules when operation field is invalid', async () => { - const { body: createdRule } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send(getTestRuleData({ tags: ['foo'] })) - .expect(200); - objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); - + it('should handle bulk edit of rules when operation value type is incorrect', async () => { const payload = { - ids: [createdRule.id], + filter: '', operations: [ { operation: 'add', - field: 'test', - value: ['test'], + field: 'tags', + value: 'not an array', }, ], }; @@ -482,7 +475,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - '[request body.operations.0]: types that failed validation:\n- [request body.operations.0.0.field]: expected value to equal [tags]\n- [request body.operations.0.1.field]: expected value to equal [actions]', + '[request body.operations.0]: types that failed validation:\n- [request body.operations.0.0.value]: could not parse array value from json input\n- [request body.operations.0.1.field]: expected value to equal [actions]\n- [request body.operations.0.2.operation]: expected value to equal [set]\n- [request body.operations.0.3.operation]: expected value to equal [set]\n- [request body.operations.0.4.operation]: expected value to equal [set]', }); expect(response.statusCode).to.eql(400); break; @@ -520,7 +513,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - '[request body.operations.0]: types that failed validation:\n- [request body.operations.0.0.field]: expected value to equal [tags]\n- [request body.operations.0.1.field]: expected value to equal [actions]', + '[request body.operations.0]: types that failed validation:\n- [request body.operations.0.0.field]: expected value to equal [tags]\n- [request body.operations.0.1.field]: expected value to equal [actions]\n- [request body.operations.0.2.operation]: expected value to equal [set]\n- [request body.operations.0.3.operation]: expected value to equal [set]\n- [request body.operations.0.4.operation]: expected value to equal [set]', }); expect(response.statusCode).to.eql(400); break; diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts index 2d98a58d29d7c..539cef69d92eb 100644 --- a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts @@ -111,6 +111,31 @@ export function initRoutes( } ); + router.post( + { + path: `/api/sample_tasks/bulk_update_schedules`, + validate: { + body: schema.object({ + taskIds: schema.arrayOf(schema.string()), + schedule: schema.object({ interval: schema.string() }), + }), + }, + }, + async function ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ) { + const { taskIds, schedule } = req.body; + try { + const taskManager = await taskManagerStart; + return res.ok({ body: await taskManager.bulkUpdateSchedules(taskIds, schedule) }); + } catch (err) { + return res.ok({ body: { taskIds, error: `${err}` } }); + } + } + ); + router.post( { path: `/api/sample_tasks/ephemeral_run_now`, diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management.ts index 51fb161ac0d6a..a649cec15d6cd 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management.ts @@ -5,12 +5,13 @@ * 2.0. */ +import moment from 'moment'; import { random, times } from 'lodash'; import expect from '@kbn/expect'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import TaskManagerMapping from '@kbn/task-manager-plugin/server/saved_objects/mappings.json'; import { DEFAULT_MAX_WORKERS, DEFAULT_POLL_INTERVAL } from '@kbn/task-manager-plugin/server/config'; -import { ConcreteTaskInstance } from '@kbn/task-manager-plugin/server'; +import { ConcreteTaskInstance, BulkUpdateSchedulesResult } from '@kbn/task-manager-plugin/server'; import { FtrProviderContext } from '../../ftr_provider_context'; const { @@ -177,6 +178,15 @@ export default function ({ getService }: FtrProviderContext) { .then((response) => response.body); } + function bulkUpdateSchedules(taskIds: string[], schedule: { interval: string }) { + return supertest + .post('/api/sample_tasks/bulk_update_schedules') + .set('kbn-xsrf', 'xxx') + .send({ taskIds, schedule }) + .expect(200) + .then((response: { body: BulkUpdateSchedulesResult }) => response.body); + } + // TODO: Add this back in with https://github.com/elastic/kibana/issues/106139 // function runEphemeralTaskNow(task: { // taskType: string; @@ -899,6 +909,101 @@ export default function ({ getService }: FtrProviderContext) { }); }); + it('should bulk update schedules for multiple tasks', async () => { + const initialTime = Date.now(); + const tasks = await Promise.all([ + scheduleTask({ + taskType: 'sampleTask', + schedule: { interval: '1h' }, + params: {}, + }), + + scheduleTask({ + taskType: 'sampleTask', + schedule: { interval: '5m' }, + params: {}, + }), + ]); + + const taskIds = tasks.map(({ id }) => id); + + await retry.try(async () => { + // ensure each task has ran at least once and been rescheduled for future run + for (const task of tasks) { + const { state } = await currentTask<{ count: number }>(task.id); + expect(state.count).to.be(1); + } + + // first task to be scheduled in 1h + expect(Date.parse((await currentTask(tasks[0].id)).runAt) - initialTime).to.be.greaterThan( + moment.duration(1, 'hour').asMilliseconds() + ); + + // second task to be scheduled in 5m + expect(Date.parse((await currentTask(tasks[1].id)).runAt) - initialTime).to.be.greaterThan( + moment.duration(5, 'minutes').asMilliseconds() + ); + }); + + await retry.try(async () => { + const updates = await bulkUpdateSchedules(taskIds, { interval: '3h' }); + + expect(updates.tasks.length).to.be(2); + expect(updates.errors.length).to.be(0); + }); + + await retry.try(async () => { + const updatedTasks = (await currentTasks()).docs; + + updatedTasks.forEach((task) => { + expect(task.schedule).to.eql({ interval: '3h' }); + // should be scheduled to run in 3 hours + expect(Date.parse(task.runAt) - initialTime).to.be.greaterThan( + moment.duration(3, 'hours').asMilliseconds() + ); + }); + }); + }); + + it('should not bulk update schedules for task in running status', async () => { + // this task should be in running status for 60s until it will be time outed + const longRunningTask = await scheduleTask({ + taskType: 'sampleRecurringTaskWhichHangs', + schedule: { interval: '1h' }, + params: {}, + }); + + runTaskNow({ id: longRunningTask.id }); + + let scheduledRunAt: string; + // ensure task is running and store scheduled runAt + await retry.try(async () => { + const task = await currentTask(longRunningTask.id); + + expect(task.status).to.be('running'); + + scheduledRunAt = task.runAt; + }); + + await retry.try(async () => { + const updates = await bulkUpdateSchedules([longRunningTask.id], { interval: '3h' }); + + // length should be 0, as task in running status won't be updated + expect(updates.tasks.length).to.be(0); + expect(updates.errors.length).to.be(0); + }); + + // ensure task wasn't updated + await retry.try(async () => { + const task = await currentTask(longRunningTask.id); + + // interval shouldn't be changed + expect(task.schedule).to.eql({ interval: '1h' }); + + // scheduledRunAt shouldn't be changed + expect(task.runAt).to.eql(scheduledRunAt); + }); + }); // TODO: Add this back in with https://github.com/elastic/kibana/issues/106139 // it('should return the resulting task state when asked to run an ephemeral task now', async () => { // const ephemeralTask = await runEphemeralTaskNow({ From d92440e6312b0e3fe78f40b8e3b0e4e704524be6 Mon Sep 17 00:00:00 2001 From: Tre Date: Mon, 13 Jun 2022 15:40:59 +0100 Subject: [PATCH 35/62] [QA][Code Coverage] Modularize Code Coverage (#133759) Kinda sneaky...since we already mutate the jest portion of the file system (target/kibana-coverage/jest) by dumping "jest unit" & "jest integration" coverage into the same "final" directory...go ahead an make "jest integration" use the same ran file designator as "jest unit". This saves me from having to add logic for this later on. --- .../steps/code_coverage/ftr_configs.sh | 2 +- .../scripts/steps/code_coverage/ingest.sh | 138 ++++++++++++------ .../steps/code_coverage/jest_integration.sh | 2 +- .../code_coverage/reporting/collectVcsInfo.sh | 4 +- .../code_coverage/reporting/ingestData.sh | 49 +++---- .../code_coverage/reporting/prokLinks.sh | 16 +- .../reporting/uploadStaticSite.sh | 25 ++-- .../scripts/steps/code_coverage/util.sh | 23 +-- 8 files changed, 159 insertions(+), 100 deletions(-) diff --git a/.buildkite/scripts/steps/code_coverage/ftr_configs.sh b/.buildkite/scripts/steps/code_coverage/ftr_configs.sh index 393b1fbe1c1d3..58b17791cbea8 100755 --- a/.buildkite/scripts/steps/code_coverage/ftr_configs.sh +++ b/.buildkite/scripts/steps/code_coverage/ftr_configs.sh @@ -116,7 +116,7 @@ printf "%s\n" "${results[@]}" echo "" # So the last step "knows" this config ran -uploadRanFile "ftr_configs" +uploadRanFile "functional" # Force exit 0 to ensure the next build step starts. exit 0 diff --git a/.buildkite/scripts/steps/code_coverage/ingest.sh b/.buildkite/scripts/steps/code_coverage/ingest.sh index a39097f706262..34d54c4d61b09 100755 --- a/.buildkite/scripts/steps/code_coverage/ingest.sh +++ b/.buildkite/scripts/steps/code_coverage/ingest.sh @@ -8,59 +8,103 @@ source .buildkite/scripts/steps/code_coverage/merge.sh export CODE_COVERAGE=1 echo "--- Reading Kibana stats cluster creds from vault" -export USER_FROM_VAULT="$(retry 5 5 vault read -field=username secret/kibana-issues/prod/coverage/elasticsearch)" -export PASS_FROM_VAULT="$(retry 5 5 vault read -field=password secret/kibana-issues/prod/coverage/elasticsearch)" -export HOST_FROM_VAULT="$(retry 5 5 vault read -field=host secret/kibana-issues/prod/coverage/elasticsearch)" -export TIME_STAMP=$(date +"%Y-%m-%dT%H:%M:00Z") - -echo "--- Print KIBANA_DIR" -echo "### KIBANA_DIR: $KIBANA_DIR" +USER_FROM_VAULT="$(retry 5 5 vault read -field=username secret/kibana-issues/prod/coverage/elasticsearch)" +export USER_FROM_VAULT +PASS_FROM_VAULT="$(retry 5 5 vault read -field=password secret/kibana-issues/prod/coverage/elasticsearch)" +export PASS_FROM_VAULT +HOST_FROM_VAULT="$(retry 5 5 vault read -field=host secret/kibana-issues/prod/coverage/elasticsearch)" +export HOST_FROM_VAULT +TIME_STAMP=$(date +"%Y-%m-%dT%H:%M:00Z") +export TIME_STAMP echo "--- Download previous git sha" .buildkite/scripts/steps/code_coverage/reporting/downloadPrevSha.sh -previousSha=$(cat downloaded_previous.txt) +PREVIOUS_SHA=$(cat downloaded_previous.txt) echo "--- Upload new git sha" .buildkite/scripts/steps/code_coverage/reporting/uploadPrevSha.sh .buildkite/scripts/bootstrap.sh -echo "--- Download coverage artifacts" -buildkite-agent artifact download target/kibana-coverage/jest/* . -#buildkite-agent artifact download target/kibana-coverage/functional/* . -buildkite-agent artifact download target/ran_files/* . -ls -l target/ran_files/* || echo "### No ran-files found" - -echo "--- process HTML Links" -.buildkite/scripts/steps/code_coverage/reporting/prokLinks.sh - -echo "--- collect VCS Info" -.buildkite/scripts/steps/code_coverage/reporting/collectVcsInfo.sh - -echo "--- Jest: Reset file paths prefix, merge coverage files, and generate the final combined report" -# Jest: Reset file paths prefix to Kibana Dir of final worker -replacePaths "$KIBANA_DIR/target/kibana-coverage/jest" "CC_REPLACEMENT_ANCHOR" "$KIBANA_DIR" -yarn nyc report --nycrc-path src/dev/code_coverage/nyc_config/nyc.jest.config.js - -#echo "--- Functional: Reset file paths prefix, merge coverage files, and generate the final combined report" -# Functional: Reset file paths prefix to Kibana Dir of final worker -#set +e -#sed -ie "s|CC_REPLACEMENT_ANCHOR|${KIBANA_DIR}|g" target/kibana-coverage/functional/*.json -#echo "--- Begin Split and Merge for Functional" -#splitCoverage target/kibana-coverage/functional -#splitMerge -#set -e - -echo "--- Archive and upload combined reports" -collectAndUpload target/kibana-coverage/jest/kibana-jest-coverage.tar.gz \ - target/kibana-coverage/jest-combined -#collectAndUpload target/kibana-coverage/functional/kibana-functional-coverage.tar.gz \ -# target/kibana-coverage/functional-combined - -echo "--- Upload coverage static site" -.buildkite/scripts/steps/code_coverage/reporting/uploadStaticSite.sh - -echo "--- Ingest results to Kibana stats cluster" -.buildkite/scripts/steps/code_coverage/reporting/ingestData.sh 'elastic+kibana+code-coverage' \ - ${BUILDKITE_BUILD_NUMBER} ${BUILDKITE_BUILD_URL} ${previousSha} \ - 'src/dev/code_coverage/ingest_coverage/team_assignment/team_assignments.txt' +collectRan() { + buildkite-agent artifact download target/ran_files/* . + + while read -r x; do + ran=("${ran[@]}" "$(cat "$x")") + done <<<"$(find target/ran_files -maxdepth 1 -type f -name '*.txt')" + + echo "--- Collected Ran files: ${ran[*]}" +} + +uniqueifyRanConfigs() { + local xs=("$@") + local xss + xss=$(printf "%s\n" "${xs[@]}" | sort -u | tr '\n' ' ' | xargs) # xargs trims whitespace + uniqRanConfigs=("$xss") + echo "--- Uniq Ran files: ${uniqRanConfigs[*]}" +} + +fetchArtifacts() { + echo "--- Fetch coverage artifacts" + + local xs=("$@") + for x in "${xs[@]}"; do + buildkite-agent artifact download "target/kibana-coverage/${x}/*" . + done +} + +archiveReports() { + echo "--- Archive and upload combined reports" + + local xs=("$@") + for x in "${xs[@]}"; do + echo "### Collect and Upload for: ${x}" +# fileHeads "target/file-heads-archive-reports-for-${x}.txt" "target/kibana-coverage/${x}" +# dirListing "target/dir-listing-${x}-combined-during-archiveReports.txt" target/kibana-coverage/${x}-combined +# dirListing "target/dir-listing-${x}-during-archiveReports.txt" target/kibana-coverage/${x} + collectAndUpload "target/kibana-coverage/${x}/kibana-${x}-coverage.tar.gz" "target/kibana-coverage/${x}-combined" + done +} + +mergeAll() { + local xs=("$@") + + for x in "${xs[@]}"; do + if [ "$x" == "jest" ]; then + echo "--- [$x]: Reset file paths prefix, merge coverage files, and generate the final combined report" + replacePaths "$KIBANA_DIR/target/kibana-coverage/jest" "CC_REPLACEMENT_ANCHOR" "$KIBANA_DIR" + yarn nyc report --nycrc-path src/dev/code_coverage/nyc_config/nyc.jest.config.js + elif [ "$x" == "functional" ]; then + echo "---[$x] : Reset file paths prefix, merge coverage files, and generate the final combined report" + set +e + sed -ie "s|CC_REPLACEMENT_ANCHOR|${KIBANA_DIR}|g" target/kibana-coverage/functional/*.json + echo "--- Begin Split and Merge for Functional" + splitCoverage target/kibana-coverage/functional + splitMerge + set -e + fi + done +} + +modularize() { + collectRan + if [ -d target/ran_files ]; then + uniqueifyRanConfigs "${ran[@]}" + fetchArtifacts "${uniqRanConfigs[@]}" + mergeAll "${uniqRanConfigs[@]}" + archiveReports "${uniqRanConfigs[@]}" + .buildkite/scripts/steps/code_coverage/reporting/prokLinks.sh "${uniqRanConfigs[@]}" + .buildkite/scripts/steps/code_coverage/reporting/uploadStaticSite.sh "${uniqRanConfigs[@]}" + .buildkite/scripts/steps/code_coverage/reporting/collectVcsInfo.sh + source .buildkite/scripts/steps/code_coverage/reporting/ingestData.sh 'elastic+kibana+code-coverage' \ + "${BUILDKITE_BUILD_NUMBER}" "${BUILDKITE_BUILD_URL}" "${PREVIOUS_SHA}" \ + 'src/dev/code_coverage/ingest_coverage/team_assignment/team_assignments.txt' + ingestModular "${uniqRanConfigs[@]}" + else + echo "--- Found zero configs that ran, cancelling ingestion." + exit 11 + fi +} + +modularize +echo "### unique ran configs: ${uniqRanConfigs[*]}" diff --git a/.buildkite/scripts/steps/code_coverage/jest_integration.sh b/.buildkite/scripts/steps/code_coverage/jest_integration.sh index cf4b422a1d46d..cb59d15c612fc 100755 --- a/.buildkite/scripts/steps/code_coverage/jest_integration.sh +++ b/.buildkite/scripts/steps/code_coverage/jest_integration.sh @@ -15,4 +15,4 @@ echo '--- Jest Integration code coverage' .buildkite/scripts/steps/code_coverage/jest_parallel.sh jest.integration.config.js # So the last step "knows" this config ran -uploadRanFile "jest_integration" +uploadRanFile "jest" diff --git a/.buildkite/scripts/steps/code_coverage/reporting/collectVcsInfo.sh b/.buildkite/scripts/steps/code_coverage/reporting/collectVcsInfo.sh index 4e6b3907b6e34..098afa7dc9c61 100755 --- a/.buildkite/scripts/steps/code_coverage/reporting/collectVcsInfo.sh +++ b/.buildkite/scripts/steps/code_coverage/reporting/collectVcsInfo.sh @@ -2,6 +2,8 @@ set -euo pipefail +echo "--- collect VCS Info" + echo "### Prok'd Index File: ..." cat src/dev/code_coverage/www/index.html @@ -27,4 +29,4 @@ for X in "${!XS[@]}"; do } done echo "### VCS_INFO:" -cat VCS_INFO.txt \ No newline at end of file +cat VCS_INFO.txt diff --git a/.buildkite/scripts/steps/code_coverage/reporting/ingestData.sh b/.buildkite/scripts/steps/code_coverage/reporting/ingestData.sh index de006352d0b09..7eac3727cfc60 100755 --- a/.buildkite/scripts/steps/code_coverage/reporting/ingestData.sh +++ b/.buildkite/scripts/steps/code_coverage/reporting/ingestData.sh @@ -2,9 +2,6 @@ set -euo pipefail -echo "### Ingesting Code Coverage" -echo "" - COVERAGE_JOB_NAME=$1 export COVERAGE_JOB_NAME echo "### debug COVERAGE_JOB_NAME: ${COVERAGE_JOB_NAME}" @@ -31,27 +28,25 @@ echo "### debug TEAM_ASSIGN_PATH: ${TEAM_ASSIGN_PATH}" BUFFER_SIZE=500 export BUFFER_SIZE -echo "### debug BUFFER_SIZE: ${BUFFER_SIZE}" - -# Build team assignments file -echo "### Generate Team Assignments" -CI_STATS_DISABLED=true node scripts/generate_team_assignments.js \ - --verbose --src '.github/CODEOWNERS' --dest $TEAM_ASSIGN_PATH - -#for x in functional jest; do -# echo "### Ingesting coverage for ${x}" -# COVERAGE_SUMMARY_FILE="target/kibana-coverage/${x}-combined/coverage-summary.json" -# -# CI_STATS_DISABLED=true node scripts/ingest_coverage.js --path ${COVERAGE_SUMMARY_FILE} \ -# --vcsInfoPath ./VCS_INFO.txt --teamAssignmentsPath $TEAM_ASSIGN_PATH & -#done -#wait - -echo "### Ingesting coverage for JEST" -COVERAGE_SUMMARY_FILE="target/kibana-coverage/jest-combined/coverage-summary.json" - -CI_STATS_DISABLED=true node scripts/ingest_coverage.js --path ${COVERAGE_SUMMARY_FILE} \ - --vcsInfoPath ./VCS_INFO.txt --teamAssignmentsPath $TEAM_ASSIGN_PATH - -echo "--- Ingesting Code Coverage - Complete" -echo "" + +ingestModular() { + local xs=("$@") + + echo "--- Generate Team Assignments" + CI_STATS_DISABLED=true node scripts/generate_team_assignments.js \ + --verbose --src '.github/CODEOWNERS' --dest "$TEAM_ASSIGN_PATH" + + echo "--- Ingest results to Kibana stats cluster" + for x in "${xs[@]}"; do + echo "--- Ingesting coverage for ${x}" + + COVERAGE_SUMMARY_FILE="target/kibana-coverage/${x}-combined/coverage-summary.json" + + CI_STATS_DISABLED=true node scripts/ingest_coverage.js --path "${COVERAGE_SUMMARY_FILE}" \ + --vcsInfoPath ./VCS_INFO.txt --teamAssignmentsPath "$TEAM_ASSIGN_PATH" & + done + wait + + echo "--- Ingesting Code Coverage - Complete" + echo "" +} diff --git a/.buildkite/scripts/steps/code_coverage/reporting/prokLinks.sh b/.buildkite/scripts/steps/code_coverage/reporting/prokLinks.sh index 0b6d0ce8ea105..f7d03b5730b87 100755 --- a/.buildkite/scripts/steps/code_coverage/reporting/prokLinks.sh +++ b/.buildkite/scripts/steps/code_coverage/reporting/prokLinks.sh @@ -2,8 +2,20 @@ set -euo pipefail -cat << EOF > src/dev/code_coverage/www/index_partial_2.html -
    Latest Jest +echo "--- process HTML Links" + +xs=("$@") +len=${#xs[@]} + +# TODO-TRE: Maybe use more exhaustive logic instead of just length. +if [[ $len -eq 2 ]]; then + links="Latest JestLatest FTR" +else + links="Latest Jest" +fi + +cat <src/dev/code_coverage/www/index_partial_2.html + ${links} diff --git a/.buildkite/scripts/steps/code_coverage/reporting/uploadStaticSite.sh b/.buildkite/scripts/steps/code_coverage/reporting/uploadStaticSite.sh index dcb0b03b16d7c..02f2262075b89 100755 --- a/.buildkite/scripts/steps/code_coverage/reporting/uploadStaticSite.sh +++ b/.buildkite/scripts/steps/code_coverage/reporting/uploadStaticSite.sh @@ -2,19 +2,22 @@ set -euo pipefail +xs=("$@") + uploadPrefix="gs://elastic-bekitzur-kibana-coverage-live/" uploadPrefixWithTimeStamp="${uploadPrefix}${TIME_STAMP}/" -cat src/dev/code_coverage/www/index.html - -for x in 'src/dev/code_coverage/www/index.html' 'src/dev/code_coverage/www/404.html'; do - gsutil -m -q cp -r -a public-read -z js,css,html ${x} ${uploadPrefix} -done +uploadBase() { + for x in 'src/dev/code_coverage/www/index.html' 'src/dev/code_coverage/www/404.html'; do + gsutil -m -q cp -r -a public-read -z js,css,html "${x}" "${uploadPrefix}" + done +} -#gsutil -m -q cp -r -a public-read -z js,css,html ${x} ${uploadPrefixWithTimeStamp} -# -#for x in 'target/kibana-coverage/functional-combined' 'target/kibana-coverage/jest-combined'; do -# gsutil -m -q cp -r -a public-read -z js,css,html ${x} ${uploadPrefixWithTimeStamp} -#done +uploadRest() { + for x in "${xs[@]}"; do + gsutil -m -q cp -r -a public-read -z js,css,html "target/kibana-coverage/${x}-combined" "${uploadPrefixWithTimeStamp}" + done +} -gsutil -m -q cp -r -a public-read -z js,css,html 'target/kibana-coverage/jest-combined' ${uploadPrefixWithTimeStamp} +uploadBase +uploadRest diff --git a/.buildkite/scripts/steps/code_coverage/util.sh b/.buildkite/scripts/steps/code_coverage/util.sh index e7da75bb7573d..cb48d62695854 100755 --- a/.buildkite/scripts/steps/code_coverage/util.sh +++ b/.buildkite/scripts/steps/code_coverage/util.sh @@ -2,15 +2,27 @@ set -euo pipefail +header() { + local fileName=$1 + + echo "" >"$fileName" + + echo "### File Name:" >>"$fileName" + printf " %s\n\n" "$fileName" >>"$fileName" +} + # $1 file name, ex: "target/dir-listing-jest.txt" # $2 directory to be listed, ex: target/kibana-coverage/jest dirListing() { local fileName=$1 local dir=$2 - ls -l "$dir" >"$fileName" + header "$fileName" + + ls -l "$dir" >>"$fileName" printf "\n### %s \n\tlisted to: %s\n" "$dir" "$fileName" + buildkite-agent artifact upload "$fileName" printf "\n### %s Uploaded\n" "$fileName" @@ -29,15 +41,6 @@ replacePaths() { done } -header() { - local fileName=$1 - - echo "" >"$fileName" - - echo "### File Name:" >>"$fileName" - printf "\t%s\n" "$fileName" >>"$fileName" -} - fileHeads() { local fileName=$1 local dir=$2 From af9ca6afb9073bae11b9b1801d7f65779ddd6821 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 13 Jun 2022 10:50:56 -0400 Subject: [PATCH 36/62] [Fleet] Remove invalid flag from README (#134213) --- x-pack/plugins/fleet/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/README.md b/x-pack/plugins/fleet/README.md index 20433cf72b5c1..5b7a7fcb00039 100644 --- a/x-pack/plugins/fleet/README.md +++ b/x-pack/plugins/fleet/README.md @@ -40,7 +40,7 @@ One common development workflow is: ``` - Start Kibana in another shell ``` - yarn start --xpack.fleet.enabled=true --no-base-path + yarn start --no-base-path ``` This plugin follows the `common`, `server`, `public` structure from the [Architecture Style Guide From b55a30239e3f2a281fac96d4bc3421f932c00940 Mon Sep 17 00:00:00 2001 From: Tre Date: Mon, 13 Jun 2022 15:52:32 +0100 Subject: [PATCH 37/62] [Archive Migration] x-pack global_search/basic (#134191) * [Archive Migrations] x-pack/test/plugin_functional/es_archives/global_search/basic * Add missing reference. --- .../kbn_archiver/global_search/basic.json | 191 +++++++ .../es_archives/global_search/basic/data.json | 193 ------- .../global_search/basic/mappings.json | 478 ------------------ .../global_search/global_search_providers.ts | 9 +- 4 files changed, 197 insertions(+), 674 deletions(-) create mode 100644 x-pack/test/functional/fixtures/kbn_archiver/global_search/basic.json delete mode 100644 x-pack/test/plugin_functional/es_archives/global_search/basic/data.json delete mode 100644 x-pack/test/plugin_functional/es_archives/global_search/basic/mappings.json diff --git a/x-pack/test/functional/fixtures/kbn_archiver/global_search/basic.json b/x-pack/test/functional/fixtures/kbn_archiver/global_search/basic.json new file mode 100644 index 0000000000000..49f26e7429870 --- /dev/null +++ b/x-pack/test/functional/fixtures/kbn_archiver/global_search/basic.json @@ -0,0 +1,191 @@ +{ + "attributes": { + "fieldFormatMap": "{\"machine.ram\":{\"id\":\"number\",\"params\":{\"pattern\":\"0,0.[000] b\"}}}", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":3,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":3,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "timeFieldName": "@timestamp", + "title": "to-be-deleted" + }, + "coreMigrationVersion": "7.14.0", + "id": "b15b1d40-a8bb-11e9-98cf-2bb06ef63e0b", + "references": [], + "type": "index-pattern", + "updated_at": "2018-04-16T16:57:12.263Z", + "version": "WzE3NiwxXQ==" +} + +{ + "attributes": { + "description": "", + "layerListJSON": "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\",\"lightModeDefault\":\"road_map\"},\"id\":\"c7bdee60-5267-459e-83d6-b53acf1b9e67\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"EMS_VECTOR_TILE\"}]", + "mapStateJSON": "{\"zoom\":0.8,\"center\":{\"lon\":0,\"lat\":0},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":false,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"settings\":{\"autoFitToDataBounds\":false}}", + "title": "just a map", + "uiStateJSON": "{\"isLayerTOCOpen\":true}" + }, + "coreMigrationVersion": "8.4.0", + "id": "0b849ed0-70f5-11e9-8625-9580c4904684", + "migrationVersion": { + "map": "8.1.0" + }, + "references": [], + "type": "map", + "updated_at": "2019-05-07T18:22:17.405Z", + "version": "WzExLDJd" +} + +{ + "attributes": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "optionsJSON": "{\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[{\"version\":\"8.0.0\",\"type\":\"map\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1\"},\"panelIndex\":\"1\",\"embeddableConfig\":{\"mapCenter\":{\"lat\":0,\"lon\":0,\"zoom\":0.8},\"isLayerTOCOpen\":true,\"enhancements\":{}},\"panelRefName\":\"panel_1\"}]", + "timeRestore": false, + "title": "dashboard with map", + "version": 1 + }, + "coreMigrationVersion": "8.4.0", + "id": "1c1a87f0-70f5-11e9-8625-9580c4904684", + "migrationVersion": { + "dashboard": "8.3.0" + }, + "references": [ + { + "id": "0b849ed0-70f5-11e9-8625-9580c4904684", + "name": "1:panel_1", + "type": "map" + } + ], + "type": "dashboard", + "updated_at": "2019-05-07T18:22:45.231Z", + "version": "WzEyLDJd" +} + +{ + "attributes": { + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "coreMigrationVersion": "8.4.0", + "id": "logstash-*", + "migrationVersion": { + "index-pattern": "8.0.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2018-12-21T00:43:07.096Z", + "version": "WzcsMl0=" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "A Pie", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}},\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"distinctColors\":true,\"legendDisplay\":\"show\",\"legendSize\":\"auto\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}" + }, + "coreMigrationVersion": "8.4.0", + "id": "75c3e060-1e7c-11e9-8488-65449e65d0ed", + "migrationVersion": { + "visualization": "8.3.0" + }, + "references": [ + { + "id": "logstash-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2019-01-22T19:32:31.206Z", + "version": "WzgsMl0=" +} + +{ + "attributes": { + "description": "Ok responses for jpg files", + "filters": [ + { + "$state": { + "store": "appState" + }, + "meta": { + "alias": null, + "disabled": false, + "index": "cb4970f5-d451-4979-b8b1-c495ef7e0f6f", + "key": "extension.raw", + "negate": false, + "params": { + "query": "jpg" + }, + "type": "phrase", + "value": "jpg" + }, + "query": { + "match": { + "extension.raw": { + "query": "jpg", + "type": "phrase" + } + } + } + } + ], + "query": { + "language": "kuery", + "query": "response:200" + }, + "title": "OKJpgs" + }, + "coreMigrationVersion": "8.4.0", + "id": "OKJpgs", + "migrationVersion": { + "query": "8.0.0" + }, + "references": [ + { + "id": "b15b1d40-a8bb-11e9-98cf-2bb06ef63e0b", + "name": "to-be-deleted", + "type": "index-pattern" + } + ], + "type": "query", + "updated_at": "2019-07-17T17:54:26.378Z", + "version": "WzEzLDJd" +} + +{ + "attributes": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"}]", + "timeRestore": false, + "title": "Amazing Dashboard", + "version": 1 + }, + "coreMigrationVersion": "8.4.0", + "id": "i-exist", + "migrationVersion": { + "dashboard": "8.3.0" + }, + "references": [ + { + "id": "75c3e060-1e7c-11e9-8488-65449e65d0ed", + "name": "1:panel_1", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2019-01-22T19:32:47.232Z", + "version": "WzksMl0=" +} diff --git a/x-pack/test/plugin_functional/es_archives/global_search/basic/data.json b/x-pack/test/plugin_functional/es_archives/global_search/basic/data.json deleted file mode 100644 index 97064dade912e..0000000000000 --- a/x-pack/test/plugin_functional/es_archives/global_search/basic/data.json +++ /dev/null @@ -1,193 +0,0 @@ -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "index-pattern:logstash-*", - "source": { - "index-pattern": { - "title": "logstash-*", - "timeFieldName": "@timestamp", - "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" - }, - "type": "index-pattern", - "migrationVersion": { - "index-pattern": "6.5.0" - }, - "updated_at": "2018-12-21T00:43:07.096Z" - } - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "visualization:75c3e060-1e7c-11e9-8488-65449e65d0ed", - "source": { - "visualization": { - "title": "A Pie", - "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", - "uiStateJSON": "{}", - "description": "", - "version": 1, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" - } - }, - "type": "visualization", - "updated_at": "2019-01-22T19:32:31.206Z" - } - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "dashboard:i-exist", - "source": { - "dashboard": { - "title": "Amazing Dashboard", - "hits": 0, - "description": "", - "panelsJSON": "[{\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"version\":\"7.0.0\",\"panelIndex\":\"1\",\"type\":\"visualization\",\"id\":\"75c3e060-1e7c-11e9-8488-65449e65d0ed\",\"embeddableConfig\":{}}]", - "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", - "version": 1, - "timeRestore": false, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" - } - }, - "type": "dashboard", - "updated_at": "2019-01-22T19:32:47.232Z" - } - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "config:6.0.0", - "source": { - "config": { - "buildNum": 9007199254740991, - "defaultIndex": "logstash-*" - }, - "type": "config", - "updated_at": "2019-01-22T19:32:02.235Z" - } - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "map:0b849ed0-70f5-11e9-8625-9580c4904684", - "source": { - "map": { - "title" : "just a map", - "description" : "", - "mapStateJSON" : "{\"zoom\":0.8,\"center\":{\"lon\":0,\"lat\":0},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":false,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"}}", - "layerListJSON" : "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"id\":\"c7bdee60-5267-459e-83d6-b53acf1b9e67\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"applyGlobalQuery\":true,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\"}]", - "uiStateJSON" : "{\"isLayerTOCOpen\":true}", - "bounds" : { - "type" : "polygon", - "coordinates" : [ - [ - [ - -180, - 85.05113 - ], - [ - -180, - -85.05113 - ], - [ - 180, - -85.05113 - ], - [ - 180, - 85.05113 - ], - [ - -180, - 85.05113 - ] - ] - ] - } - }, - "type": "map", - "references" : [ ], - "migrationVersion" : { - "map" : "7.1.0" - }, - "updated_at" : "2019-05-07T18:22:17.405Z" - } - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "dashboard:1c1a87f0-70f5-11e9-8625-9580c4904684", - "source": { - "dashboard": { - "title" : "dashboard with map", - "hits" : 0, - "description" : "", - "panelsJSON" : "[{\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1\"},\"version\":\"8.0.0\",\"panelIndex\":\"1\",\"embeddableConfig\":{\"mapCenter\":{\"lat\":0,\"lon\":0,\"zoom\":0.8},\"isLayerTOCOpen\":true},\"panelRefName\":\"panel_0\"}]", - "optionsJSON" : "{\"useMargins\":true,\"hidePanelTitles\":false}", - "version" : 1, - "timeRestore" : false, - "kibanaSavedObjectMeta" : { - "searchSourceJSON" : "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" - } - }, - "type": "dashboard", - "references" : [ - { - "name" : "panel_0", - "type" : "map", - "id" : "0b849ed0-70f5-11e9-8625-9580c4904684" - } - ], - "migrationVersion" : { - "dashboard" : "7.0.0" - }, - "updated_at" : "2019-05-07T18:22:45.231Z" - } - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "query:OKJpgs", - "source": { - "query": { - "title": "OKJpgs", - "description": "Ok responses for jpg files", - "query": { - "query": "response:200", - "language": "kuery" - }, - "filters": [{"meta":{"index":"b15b1d40-a8bb-11e9-98cf-2bb06ef63e0b","alias":null,"negate":false,"type":"phrase","key":"extension.raw","value":"jpg","params":{"query":"jpg"},"disabled":false},"query":{"match":{"extension.raw":{"query":"jpg","type":"phrase"}}},"$state":{"store":"appState"}}] - }, - "type": "query", - "updated_at": "2019-07-17T17:54:26.378Z" - } - } -} diff --git a/x-pack/test/plugin_functional/es_archives/global_search/basic/mappings.json b/x-pack/test/plugin_functional/es_archives/global_search/basic/mappings.json deleted file mode 100644 index a4392cd88b356..0000000000000 --- a/x-pack/test/plugin_functional/es_archives/global_search/basic/mappings.json +++ /dev/null @@ -1,478 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - ".kibana": {} - }, - "index": ".kibana_1", - "settings": { - "index": { - "number_of_shards": "1", - "auto_expand_replicas": "0-1", - "number_of_replicas": "0" - } - }, - "mappings": { - "dynamic": "strict", - "properties": { - "apm-telemetry": { - "properties": { - "has_any_services": { - "type": "boolean" - }, - "services_per_agent": { - "properties": { - "go": { - "type": "long", - "null_value": 0 - }, - "java": { - "type": "long", - "null_value": 0 - }, - "js-base": { - "type": "long", - "null_value": 0 - }, - "nodejs": { - "type": "long", - "null_value": 0 - }, - "python": { - "type": "long", - "null_value": 0 - }, - "ruby": { - "type": "long", - "null_value": 0 - } - } - } - } - }, - "canvas-workpad": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "id": { - "type": "text", - "index": false - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } - } - } - }, - "config": { - "dynamic": "true", - "properties": { - "accessibility:disableAnimations": { - "type": "boolean" - }, - "buildNum": { - "type": "keyword" - }, - "dateFormat:tz": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "defaultIndex": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "telemetry:optIn": { - "type": "boolean" - } - } - }, - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "map" : { - "properties" : { - "bounds": { - "dynamic": false, - "properties": {} - }, - "description" : { - "type" : "text" - }, - "layerListJSON" : { - "type" : "text" - }, - "mapStateJSON" : { - "type" : "text" - }, - "title" : { - "type" : "text" - }, - "uiStateJSON" : { - "type" : "text" - }, - "version" : { - "type" : "integer" - } - } - }, - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "typeMeta": { - "type": "keyword" - } - } - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "index-pattern": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "space": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - } - } - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "index-pattern": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "namespace": { - "type": "keyword" - }, - "references": { - "type": "nested", - "properties": { - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "id": { - "type": "keyword" - } - } - }, - "search": { - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "initials": { - "type": "keyword" - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } - } - } - }, - "spaceId": { - "type": "keyword" - }, - "telemetry": { - "properties": { - "enabled": { - "type": "boolean" - } - } - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchId": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - }, - "query": { - "properties": { - "title": { - "type": "text" - }, - "description": { - "type": "text" - }, - "query": { - "properties": { - "language": { - "type": "keyword" - }, - "query": { - "type": "keyword", - "index": false - } - } - }, - "filters": { - "type": "object", - "enabled": false - }, - "timefilter": { - "type": "object", - "enabled": false - } - } - } - } - } - } -} diff --git a/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts b/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts index fd6a46b391cb8..047b852612ecf 100644 --- a/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts +++ b/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts @@ -13,7 +13,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const pageObjects = getPageObjects(['common']); const browser = getService('browser'); - const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); const findResultsWithApi = async (t: string): Promise => { return browser.executeAsync(async (term, cb) => { @@ -30,11 +30,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('SavedObject provider', function () { before(async () => { - await esArchiver.load('x-pack/test/plugin_functional/es_archives/global_search/basic'); + await kibanaServer.savedObjects.cleanStandardList(); + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/global_search/basic' + ); }); after(async () => { - await esArchiver.unload('x-pack/test/plugin_functional/es_archives/global_search/basic'); + await kibanaServer.savedObjects.cleanStandardList(); }); it('can search for index patterns', async () => { From f94f4237269bf02cb5e7cc8ed49316a9e480877f Mon Sep 17 00:00:00 2001 From: Katerina Patticha Date: Mon, 13 Jun 2022 16:55:05 +0200 Subject: [PATCH 38/62] [APM] Migrate api tests to apmApiClient service (#133320) * [APM] Migrate api tests to apmApiClient service * Migrate legacy service * Fix date format * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../tests/csm/has_rum_data.spec.ts | 32 +- .../tests/csm/long_task_metrics.spec.ts | 28 +- .../tests/csm/page_views.spec.ts | 58 ++-- .../tests/services/agent.spec.ts | 32 +- .../tests/services/transaction_types.spec.ts | 36 ++- .../settings/anomaly_detection/basic.spec.ts | 49 +-- .../anomaly_detection/read_user.spec.ts | 29 +- .../tests/transactions/breakdown.spec.ts | 79 +++-- .../tests/transactions/error_rate.spec.ts | 108 ++++--- .../tests/transactions/latency.spec.ts | 300 ++++++++---------- .../tests/transactions/trace_samples.spec.ts | 68 ++-- ...ransactions_groups_main_statistics.spec.ts | 43 +-- 12 files changed, 494 insertions(+), 368 deletions(-) diff --git a/x-pack/test/apm_api_integration/tests/csm/has_rum_data.spec.ts b/x-pack/test/apm_api_integration/tests/csm/has_rum_data.spec.ts index f9ba588ffccdb..5e717739c5d04 100644 --- a/x-pack/test/apm_api_integration/tests/csm/has_rum_data.spec.ts +++ b/x-pack/test/apm_api_integration/tests/csm/has_rum_data.spec.ts @@ -10,13 +10,22 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function rumHasDataApiTests({ getService }: FtrProviderContext) { const registry = getService('registry'); - const supertest = getService('legacySupertestAsApmReadUser'); + const apmApiClient = getService('apmApiClient'); registry.when('has_rum_data without data', { config: 'trial', archives: [] }, () => { it('returns empty list', async () => { - const response = await supertest.get( - '/api/apm/observability_overview/has_rum_data?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=' - ); + const start = new Date('2020-09-07T00:00:00.000Z').getTime(); + const end = new Date('2020-09-14T00:00:00.000Z').getTime() - 1; + + const response = await apmApiClient.readUser({ + endpoint: 'GET /api/apm/observability_overview/has_rum_data', + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + }, + }, + }); expect(response.status).to.be(200); expectSnapshot(response.body).toMatchInline(` @@ -33,9 +42,18 @@ export default function rumHasDataApiTests({ getService }: FtrProviderContext) { { config: 'trial', archives: ['8.0.0', 'rum_8.0.0'] }, () => { it('returns that it has data and service name with most traffic', async () => { - const response = await supertest.get( - '/api/apm/observability_overview/has_rum_data?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=' - ); + const start = new Date('2020-09-07T00:00:00.000Z').getTime(); + const end = new Date('2020-09-16T00:00:00.000Z').getTime() - 1; + + const response = await apmApiClient.readUser({ + endpoint: 'GET /api/apm/observability_overview/has_rum_data', + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + }, + }, + }); expect(response.status).to.be(200); diff --git a/x-pack/test/apm_api_integration/tests/csm/long_task_metrics.spec.ts b/x-pack/test/apm_api_integration/tests/csm/long_task_metrics.spec.ts index 756d10bc4558d..61ee97398be9a 100644 --- a/x-pack/test/apm_api_integration/tests/csm/long_task_metrics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/csm/long_task_metrics.spec.ts @@ -10,14 +10,19 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function rumServicesApiTests({ getService }: FtrProviderContext) { const registry = getService('registry'); - const supertest = getService('legacySupertestAsApmReadUser'); + const apmApiClient = getService('apmApiClient'); registry.when('CSM long task metrics without data', { config: 'trial', archives: [] }, () => { it('returns empty list', async () => { - const response = await supertest.get('/internal/apm/ux/long-task-metrics').query({ - start: '2020-09-07T20:35:54.654Z', - end: '2020-09-14T20:35:54.654Z', - uiFilters: '{"serviceName":["elastic-co-rum-test"]}', + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/ux/long-task-metrics', + params: { + query: { + start: '2020-09-07T20:35:54.654Z', + end: '2020-09-14T20:35:54.654Z', + uiFilters: '{"serviceName":["elastic-co-rum-test"]}', + }, + }, }); expect(response.status).to.be(200); @@ -34,10 +39,15 @@ export default function rumServicesApiTests({ getService }: FtrProviderContext) { config: 'trial', archives: ['8.0.0', 'rum_8.0.0'] }, () => { it('returns web core vitals values', async () => { - const response = await supertest.get('/internal/apm/ux/long-task-metrics').query({ - start: '2020-09-07T20:35:54.654Z', - end: '2020-09-16T20:35:54.654Z', - uiFilters: '{"serviceName":["kibana-frontend-8_0_0"]}', + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/ux/long-task-metrics', + params: { + query: { + start: '2020-09-07T20:35:54.654Z', + end: '2020-09-16T20:35:54.654Z', + uiFilters: '{"serviceName":["kibana-frontend-8_0_0"]}', + }, + }, }); expect(response.status).to.be(200); diff --git a/x-pack/test/apm_api_integration/tests/csm/page_views.spec.ts b/x-pack/test/apm_api_integration/tests/csm/page_views.spec.ts index f699fc9f8a3b1..31f7def2f9884 100644 --- a/x-pack/test/apm_api_integration/tests/csm/page_views.spec.ts +++ b/x-pack/test/apm_api_integration/tests/csm/page_views.spec.ts @@ -10,14 +10,19 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function rumServicesApiTests({ getService }: FtrProviderContext) { const registry = getService('registry'); - const supertest = getService('legacySupertestAsApmReadUser'); + const apmApiClient = getService('apmApiClient'); registry.when('CSM page views without data', { config: 'trial', archives: [] }, () => { it('returns empty list', async () => { - const response = await supertest.get('/internal/apm/ux/page-view-trends').query({ - start: '2020-09-07T20:35:54.654Z', - end: '2020-09-14T20:35:54.654Z', - uiFilters: '{"serviceName":["elastic-co-rum-test"]}', + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/ux/page-view-trends', + params: { + query: { + start: '2020-09-07T20:35:54.654Z', + end: '2020-09-14T20:35:54.654Z', + uiFilters: '{"serviceName":["elastic-co-rum-test"]}', + }, + }, }); expect(response.status).to.be(200); @@ -25,11 +30,16 @@ export default function rumServicesApiTests({ getService }: FtrProviderContext) }); it('returns empty list with breakdowns', async () => { - const response = await supertest.get('/internal/apm/ux/page-view-trends').query({ - start: '2020-09-07T20:35:54.654Z', - end: '2020-09-14T20:35:54.654Z', - uiFilters: '{"serviceName":["elastic-co-rum-test"]}', - breakdowns: '{"name":"Browser","fieldName":"user_agent.name","type":"category"}', + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/ux/page-view-trends', + params: { + query: { + start: '2020-09-07T20:35:54.654Z', + end: '2020-09-14T20:35:54.654Z', + uiFilters: '{"serviceName":["elastic-co-rum-test"]}', + breakdowns: '{"name":"Browser","fieldName":"user_agent.name","type":"category"}', + }, + }, }); expect(response.status).to.be(200); @@ -42,10 +52,15 @@ export default function rumServicesApiTests({ getService }: FtrProviderContext) { config: 'trial', archives: ['8.0.0', 'rum_8.0.0'] }, () => { it('returns page views', async () => { - const response = await supertest.get('/internal/apm/ux/page-view-trends').query({ - start: '2020-09-07T20:35:54.654Z', - end: '2020-09-16T20:35:54.654Z', - uiFilters: '{"serviceName":["kibana-frontend-8_0_0"]}', + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/ux/page-view-trends', + params: { + query: { + start: '2020-09-07T20:35:54.654Z', + end: '2020-09-16T20:35:54.654Z', + uiFilters: '{"serviceName":["kibana-frontend-8_0_0"]}', + }, + }, }); expect(response.status).to.be(200); @@ -53,11 +68,16 @@ export default function rumServicesApiTests({ getService }: FtrProviderContext) expectSnapshot(response.body).toMatch(); }); it('returns page views with breakdown', async () => { - const response = await supertest.get('/internal/apm/ux/page-view-trends').query({ - start: '2020-09-07T20:35:54.654Z', - end: '2020-09-16T20:35:54.654Z', - uiFilters: '{"serviceName":["kibana-frontend-8_0_0"]}', - breakdowns: '{"name":"Browser","fieldName":"user_agent.name","type":"category"}', + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/ux/page-view-trends', + params: { + query: { + start: '2020-09-07T20:35:54.654Z', + end: '2020-09-16T20:35:54.654Z', + uiFilters: '{"serviceName":["kibana-frontend-8_0_0"]}', + breakdowns: '{"name":"Browser","fieldName":"user_agent.name","type":"category"}', + }, + }, }); expect(response.status).to.be(200); diff --git a/x-pack/test/apm_api_integration/tests/services/agent.spec.ts b/x-pack/test/apm_api_integration/tests/services/agent.spec.ts index c1c1cc641c5fc..69f1938192b9e 100644 --- a/x-pack/test/apm_api_integration/tests/services/agent.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/agent.spec.ts @@ -11,18 +11,23 @@ import archives from '../../common/fixtures/es_archiver/archives_metadata'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); - const supertest = getService('legacySupertestAsApmReadUser'); + const apmApiClient = getService('apmApiClient'); const archiveName = 'apm_8.0.0'; - const range = archives[archiveName]; - const start = encodeURIComponent(range.start); - const end = encodeURIComponent(range.end); + const { start, end } = archives[archiveName]; registry.when('Agent name when data is not loaded', { config: 'basic', archives: [] }, () => { it('handles the empty state', async () => { - const response = await supertest.get( - `/internal/apm/services/opbeans-node/agent?start=${start}&end=${end}` - ); + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/agent', + params: { + path: { serviceName: 'opbeans-node' }, + query: { + start, + end, + }, + }, + }); expect(response.status).to.be(200); expect(response.body).to.eql({}); @@ -34,9 +39,16 @@ export default function ApiTest({ getService }: FtrProviderContext) { { config: 'basic', archives: [archiveName] }, () => { it('returns the agent name', async () => { - const response = await supertest.get( - `/internal/apm/services/opbeans-node/agent?start=${start}&end=${end}` - ); + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/agent', + params: { + path: { serviceName: 'opbeans-node' }, + query: { + start, + end, + }, + }, + }); expect(response.status).to.be(200); diff --git a/x-pack/test/apm_api_integration/tests/services/transaction_types.spec.ts b/x-pack/test/apm_api_integration/tests/services/transaction_types.spec.ts index 82afa69029147..bd1329cbe3e1a 100644 --- a/x-pack/test/apm_api_integration/tests/services/transaction_types.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/transaction_types.spec.ts @@ -6,28 +6,31 @@ */ import expect from '@kbn/expect'; -import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import archives from '../../common/fixtures/es_archiver/archives_metadata'; import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); - const supertest = getService('legacySupertestAsApmReadUser'); + const apmApiClient = getService('apmApiClient'); const archiveName = 'apm_8.0.0'; - const metadata = archives_metadata[archiveName]; - - // url parameters - const start = encodeURIComponent(metadata.start); - const end = encodeURIComponent(metadata.end); + const { start, end } = archives[archiveName]; registry.when( 'Transaction types when data is not loaded', { config: 'basic', archives: [] }, () => { it('handles empty state', async () => { - const response = await supertest.get( - `/internal/apm/services/opbeans-node/transaction_types?start=${start}&end=${end}` - ); + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/transaction_types', + params: { + path: { serviceName: 'opbeans-node' }, + query: { + start, + end, + }, + }, + }); expect(response.status).to.be(200); @@ -41,9 +44,16 @@ export default function ApiTest({ getService }: FtrProviderContext) { { config: 'basic', archives: [archiveName] }, () => { it('handles empty state', async () => { - const response = await supertest.get( - `/internal/apm/services/opbeans-node/transaction_types?start=${start}&end=${end}` - ); + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/transaction_types', + params: { + path: { serviceName: 'opbeans-node' }, + query: { + start, + end, + }, + }, + }); expect(response.status).to.be(200); expect(response.body.transactionTypes.length).to.be.greaterThan(0); diff --git a/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/basic.spec.ts b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/basic.spec.ts index f7c35e92e08a0..0e1e65b9c345e 100644 --- a/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/basic.spec.ts +++ b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/basic.spec.ts @@ -7,48 +7,59 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { ApmApiError } from '../../../common/apm_api_supertest'; export default function apiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); - const noAccessUser = getService('legacySupertestAsNoAccessUser'); - const readUser = getService('legacySupertestAsApmReadUser'); - const writeUser = getService('legacySupertestAsApmWriteUser'); + const apmApiClient = getService('apmApiClient'); - type SupertestAsUser = typeof noAccessUser | typeof readUser | typeof writeUser; + type SupertestAsUser = + | typeof apmApiClient.readUser + | typeof apmApiClient.writeUser + | typeof apmApiClient.noAccessUser; function getJobs(user: SupertestAsUser) { - return user.get(`/internal/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); + return user({ endpoint: `GET /internal/apm/settings/anomaly-detection/jobs` }); } function createJobs(user: SupertestAsUser, environments: string[]) { - return user - .post(`/internal/apm/settings/anomaly-detection/jobs`) - .send({ environments }) - .set('kbn-xsrf', 'foo'); + return user({ + endpoint: 'POST /internal/apm/settings/anomaly-detection/jobs', + params: { + body: { environments }, + }, + }); } async function expectForbidden(user: SupertestAsUser) { - const { body: getJobsBody } = await getJobs(user); - expect(getJobsBody.statusCode).to.be(403); - expect(getJobsBody.error).to.be('Forbidden'); - - const { body: createJobsBody } = await createJobs(user, ['production', 'staging']); + try { + await getJobs(user); + expect(true).to.be(false); + } catch (e) { + const err = e as ApmApiError; + expect(err.res.status).to.be(403); + } - expect(createJobsBody.statusCode).to.be(403); - expect(getJobsBody.error).to.be('Forbidden'); + try { + await createJobs(user, ['production', 'staging']); + expect(true).to.be(false); + } catch (e) { + const err = e as ApmApiError; + expect(err.res.status).to.be(403); + } } registry.when('ML jobs return a 403 for', { config: 'basic', archives: [] }, () => { it('user without access', async () => { - await expectForbidden(noAccessUser); + await expectForbidden(apmApiClient.noAccessUser); }); it('read user', async () => { - await expectForbidden(readUser); + await expectForbidden(apmApiClient.readUser); }); it('write user', async () => { - await expectForbidden(writeUser); + await expectForbidden(apmApiClient.writeUser); }); }); } diff --git a/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/read_user.spec.ts b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/read_user.spec.ts index 01349a503d2c5..79ccc261f72c1 100644 --- a/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/read_user.spec.ts +++ b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/read_user.spec.ts @@ -7,20 +7,24 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { ApmApiError } from '../../../common/apm_api_supertest'; export default function apiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); - const viewerUser = getService('legacySupertestAsApmReadUser'); - + const apmApiClient = getService('apmApiClient'); function getJobs() { - return viewerUser.get(`/internal/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); + return apmApiClient.writeUser({ + endpoint: `GET /internal/apm/settings/anomaly-detection/jobs`, + }); } function createJobs(environments: string[]) { - return viewerUser - .post(`/internal/apm/settings/anomaly-detection/jobs`) - .send({ environments }) - .set('kbn-xsrf', 'foo'); + return apmApiClient.readUser({ + endpoint: `POST /internal/apm/settings/anomaly-detection/jobs`, + params: { + body: { environments }, + }, + }); } registry.when('ML jobs', { config: 'trial', archives: [] }, () => { @@ -36,10 +40,13 @@ export default function apiTest({ getService }: FtrProviderContext) { describe('when calling create endpoint', () => { it('returns an error because the user does not have access', async () => { - const { body } = await createJobs(['production', 'staging']); - - expect(body.statusCode).to.be(403); - expect(body.error).to.be('Forbidden'); + try { + await createJobs(['production', 'staging']); + expect(true).to.be(false); + } catch (e) { + const err = e as ApmApiError; + expect(err.res.status).to.be(403); + } }); }); }); diff --git a/x-pack/test/apm_api_integration/tests/transactions/breakdown.spec.ts b/x-pack/test/apm_api_integration/tests/transactions/breakdown.spec.ts index c24881b5c43f4..6b7848262c69f 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/breakdown.spec.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/breakdown.spec.ts @@ -6,26 +6,34 @@ */ import expect from '@kbn/expect'; -import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import archives from '../../common/fixtures/es_archiver/archives_metadata'; import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); - const supertest = getService('legacySupertestAsApmReadUser'); + const apmApiClient = getService('apmApiClient'); const archiveName = 'apm_8.0.0'; - const metadata = archives_metadata[archiveName]; - - const start = encodeURIComponent(metadata.start); - const end = encodeURIComponent(metadata.end); + const { start, end } = archives[archiveName]; const transactionType = 'request'; const transactionName = 'GET /api'; registry.when('Breakdown when data is not loaded', { config: 'basic', archives: [] }, () => { it('handles the empty state', async () => { - const response = await supertest.get( - `/internal/apm/services/opbeans-node/transaction/charts/breakdown?start=${start}&end=${end}&transactionType=${transactionType}&environment=ENVIRONMENT_ALL&kuery=` - ); + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/transaction/charts/breakdown', + params: { + path: { serviceName: 'opbeans-node' }, + query: { + start, + end, + transactionType, + environment: 'ENVIRONMENT_ALL', + kuery: '', + }, + }, + }); + expect(response.status).to.be(200); expect(response.body).to.eql({ timeseries: [] }); }); @@ -36,17 +44,38 @@ export default function ApiTest({ getService }: FtrProviderContext) { { config: 'basic', archives: [archiveName] }, () => { it('returns the transaction breakdown for a service', async () => { - const response = await supertest.get( - `/internal/apm/services/opbeans-node/transaction/charts/breakdown?start=${start}&end=${end}&transactionType=${transactionType}&environment=ENVIRONMENT_ALL&kuery=` - ); + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/transaction/charts/breakdown', + params: { + path: { serviceName: 'opbeans-node' }, + query: { + start, + end, + transactionType, + environment: 'ENVIRONMENT_ALL', + kuery: '', + }, + }, + }); expect(response.status).to.be(200); expectSnapshot(response.body).toMatch(); }); it('returns the transaction breakdown for a transaction group', async () => { - const response = await supertest.get( - `/internal/apm/services/opbeans-node/transaction/charts/breakdown?start=${start}&end=${end}&transactionType=${transactionType}&transactionName=${transactionName}&environment=ENVIRONMENT_ALL&kuery=` - ); + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/transaction/charts/breakdown', + params: { + path: { serviceName: 'opbeans-node' }, + query: { + start, + end, + transactionType, + transactionName, + environment: 'ENVIRONMENT_ALL', + kuery: '', + }, + }, + }); expect(response.status).to.be(200); @@ -58,9 +87,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { const { title, color, type, data, hideLegend, legendValue } = timeseries[0]; - const nonNullDataPoints = data.filter((y: number | null) => y !== null); + const nonNullDataPoints = data.filter(({ y }: { y: number | null }) => y !== null); - expectSnapshot(nonNullDataPoints.length).toMatchInline(`61`); + expectSnapshot(nonNullDataPoints.length).toMatchInline(`47`); expectSnapshot( data.slice(0, 5).map(({ x, y }: { x: number; y: number | null }) => { @@ -103,9 +132,19 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(data).toMatch(); }); it('returns the transaction breakdown sorted by name', async () => { - const response = await supertest.get( - `/internal/apm/services/opbeans-node/transaction/charts/breakdown?start=${start}&end=${end}&transactionType=${transactionType}&environment=ENVIRONMENT_ALL&kuery=` - ); + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/transaction/charts/breakdown', + params: { + path: { serviceName: 'opbeans-node' }, + query: { + start, + end, + transactionType, + environment: 'ENVIRONMENT_ALL', + kuery: '', + }, + }, + }); expect(response.status).to.be(200); expectSnapshot(response.body.timeseries.map((serie: { title: string }) => serie.title)) diff --git a/x-pack/test/apm_api_integration/tests/transactions/error_rate.spec.ts b/x-pack/test/apm_api_integration/tests/transactions/error_rate.spec.ts index 54e8c442e7978..ee834a8936496 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/error_rate.spec.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/error_rate.spec.ts @@ -7,7 +7,6 @@ import expect from '@kbn/expect'; import { first, last } from 'lodash'; -import { format } from 'url'; import moment from 'moment'; import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; @@ -18,7 +17,7 @@ type ErrorRate = export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); - const supertest = getService('legacySupertestAsApmReadUser'); + const apmApiClient = getService('apmApiClient'); const archiveName = 'apm_8.0.0'; @@ -26,14 +25,41 @@ export default function ApiTest({ getService }: FtrProviderContext) { const { start, end } = archives_metadata[archiveName]; const transactionType = 'request'; + async function fetchErrorCharts({ + serviceName, + query, + }: { + serviceName: string; + query: { + start: string; + end: string; + transactionType: string; + environment: string; + kuery: string; + offset?: string; + }; + }) { + return await apmApiClient.readUser({ + endpoint: `GET /internal/apm/services/{serviceName}/transactions/charts/error_rate`, + params: { + path: { serviceName }, + query, + }, + }); + } + registry.when('Error rate when data is not loaded', { config: 'basic', archives: [] }, () => { it('handles the empty state', async () => { - const response = await supertest.get( - format({ - pathname: '/internal/apm/services/opbeans-java/transactions/charts/error_rate', - query: { start, end, transactionType, environment: 'ENVIRONMENT_ALL', kuery: '' }, - }) - ); + const response = await fetchErrorCharts({ + serviceName: 'opbeans-java', + query: { + start, + end, + transactionType, + environment: 'ENVIRONMENT_ALL', + kuery: '', + }, + }); expect(response.status).to.be(200); const body = response.body as ErrorRate; @@ -44,19 +70,17 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('handles the empty state with comparison data', async () => { - const response = await supertest.get( - format({ - pathname: '/internal/apm/services/opbeans-java/transactions/charts/error_rate', - query: { - transactionType, - start: moment(end).subtract(15, 'minutes').toISOString(), - end, - offset: '15m', - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }) - ); + const response = await fetchErrorCharts({ + serviceName: 'opbeans-java', + query: { + transactionType, + start: moment(end).subtract(15, 'minutes').toISOString(), + end, + offset: '15m', + environment: 'ENVIRONMENT_ALL', + kuery: '', + }, + }); expect(response.status).to.be(200); const body = response.body as ErrorRate; @@ -75,12 +99,17 @@ export default function ApiTest({ getService }: FtrProviderContext) { let errorRateResponse: ErrorRate; before(async () => { - const response = await supertest.get( - format({ - pathname: '/internal/apm/services/opbeans-java/transactions/charts/error_rate', - query: { start, end, transactionType, environment: 'ENVIRONMENT_ALL', kuery: '' }, - }) - ); + const response = await fetchErrorCharts({ + serviceName: 'opbeans-java', + query: { + start, + end, + transactionType, + environment: 'ENVIRONMENT_ALL', + kuery: '', + }, + }); + errorRateResponse = response.body; }); @@ -129,19 +158,18 @@ export default function ApiTest({ getService }: FtrProviderContext) { let errorRateResponse: ErrorRate; before(async () => { - const response = await supertest.get( - format({ - pathname: '/internal/apm/services/opbeans-java/transactions/charts/error_rate', - query: { - transactionType, - start: moment(end).subtract(15, 'minutes').toISOString(), - end, - offset: '15m', - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }) - ); + const response = await fetchErrorCharts({ + serviceName: 'opbeans-java', + query: { + transactionType, + start: moment(end).subtract(15, 'minutes').toISOString(), + end, + offset: '15m', + environment: 'ENVIRONMENT_ALL', + kuery: '', + }, + }); + errorRateResponse = response.body; }); diff --git a/x-pack/test/apm_api_integration/tests/transactions/latency.spec.ts b/x-pack/test/apm_api_integration/tests/transactions/latency.spec.ts index b5d9ba8102b79..bcd59423ce3cb 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/latency.spec.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/latency.spec.ts @@ -6,9 +6,9 @@ */ import expect from '@kbn/expect'; -import url from 'url'; import moment from 'moment'; import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import { LatencyAggregationType } from '@kbn/apm-plugin/common/latency_aggregation_types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; @@ -17,62 +17,52 @@ type LatencyChartReturnType = export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); - const supertest = getService('legacySupertestAsApmReadUser'); + const apmApiClient = getService('apmApiClient'); const archiveName = 'apm_8.0.0'; const { start, end } = archives_metadata[archiveName]; + async function fetchLatencyCharts({ + serviceName, + query, + }: { + serviceName: string; + query: { + start: string; + end: string; + latencyAggregationType: LatencyAggregationType; + transactionType: string; + environment: string; + kuery: string; + offset?: string; + }; + }) { + return await apmApiClient.readUser({ + endpoint: `GET /internal/apm/services/{serviceName}/transactions/charts/latency`, + params: { + path: { serviceName }, + query, + }, + }); + } + registry.when( 'Latency with a basic license when data is not loaded ', { config: 'basic', archives: [] }, () => { - it('returns 400 when latencyAggregationType is not informed', async () => { - const response = await supertest.get( - url.format({ - pathname: `/internal/apm/services/opbeans-node/transactions/charts/latency`, - query: { - start, - end, - transactionType: 'request', - environment: 'testing', - }, - }) - ); - - expect(response.status).to.be(400); - }); - - it('returns 400 when transactionType is not informed', async () => { - const response = await supertest.get( - url.format({ - pathname: `/internal/apm/services/opbeans-node/transactions/charts/latency`, - query: { - start, - end, - latencyAggregationType: 'avg', - environment: 'testing', - }, - }) - ); - - expect(response.status).to.be(400); - }); - it('handles the empty state', async () => { - const response = await supertest.get( - url.format({ - pathname: `/internal/apm/services/opbeans-node/transactions/charts/latency`, - query: { - start, - end, - latencyAggregationType: 'avg', - transactionType: 'request', - environment: 'testing', - kuery: '', - }, - }) - ); + const response = await fetchLatencyCharts({ + serviceName: 'opbeans-node', + query: { + start, + end, + latencyAggregationType: LatencyAggregationType.avg, + transactionType: 'request', + environment: 'testing', + kuery: '', + }, + }); expect(response.status).to.be(200); @@ -89,26 +79,20 @@ export default function ApiTest({ getService }: FtrProviderContext) { 'Latency with a basic license when data is loaded', { config: 'basic', archives: [archiveName] }, () => { - let response: Awaited>; - describe('average latency type', () => { - before(async () => { - response = await supertest.get( - url.format({ - pathname: `/internal/apm/services/opbeans-node/transactions/charts/latency`, - query: { - start, - end, - latencyAggregationType: 'avg', - transactionType: 'request', - environment: 'testing', - kuery: '', - }, - }) - ); - }); - it('returns average duration and timeseries', async () => { + const response = await fetchLatencyCharts({ + serviceName: 'opbeans-node', + query: { + start, + end, + latencyAggregationType: LatencyAggregationType.avg, + transactionType: 'request', + environment: 'testing', + kuery: '', + }, + }); + expect(response.status).to.be(200); const latencyChartReturn = response.body as LatencyChartReturnType; expect(latencyChartReturn.currentPeriod.overallAvgDuration).not.to.be(null); @@ -117,23 +101,19 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); describe('95th percentile latency type', () => { - before(async () => { - response = await supertest.get( - url.format({ - pathname: `/internal/apm/services/opbeans-node/transactions/charts/latency`, - query: { - start, - end, - latencyAggregationType: 'p95', - transactionType: 'request', - environment: 'testing', - kuery: '', - }, - }) - ); - }); - it('returns average duration and timeseries', async () => { + const response = await fetchLatencyCharts({ + serviceName: 'opbeans-node', + query: { + start, + end, + latencyAggregationType: LatencyAggregationType.p95, + transactionType: 'request', + environment: 'testing', + kuery: '', + }, + }); + expect(response.status).to.be(200); const latencyChartReturn = response.body as LatencyChartReturnType; expect(latencyChartReturn.currentPeriod.overallAvgDuration).not.to.be(null); @@ -142,23 +122,19 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); describe('99th percentile latency type', () => { - before(async () => { - response = await supertest.get( - url.format({ - pathname: `/internal/apm/services/opbeans-node/transactions/charts/latency`, - query: { - start, - end, - latencyAggregationType: 'p99', - transactionType: 'request', - environment: 'testing', - kuery: '', - }, - }) - ); - }); - it('returns average duration and timeseries', async () => { + const response = await fetchLatencyCharts({ + serviceName: 'opbeans-node', + query: { + start, + end, + latencyAggregationType: LatencyAggregationType.p99, + transactionType: 'request', + environment: 'testing', + kuery: '', + }, + }); + expect(response.status).to.be(200); const latencyChartReturn = response.body as LatencyChartReturnType; @@ -172,24 +148,24 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); describe('time comparison', () => { + let response: Awaited>; + before(async () => { - response = await supertest.get( - url.format({ - pathname: `/internal/apm/services/opbeans-node/transactions/charts/latency`, - query: { - latencyAggregationType: 'avg', - transactionType: 'request', - start: moment(end).subtract(15, 'minutes').toISOString(), - end, - offset: '15m', - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }) - ); + response = await fetchLatencyCharts({ + serviceName: 'opbeans-node', + query: { + latencyAggregationType: LatencyAggregationType.avg, + transactionType: 'request', + start: moment(end).subtract(15, 'minutes').toISOString(), + end, + offset: '15m', + environment: 'ENVIRONMENT_ALL', + kuery: '', + }, + }); }); - it('returns some data', async () => { + it('returns some data', () => { expect(response.status).to.be(200); const latencyChartReturn = response.body as LatencyChartReturnType; const currentPeriodNonNullDataPoints = @@ -212,20 +188,20 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); describe('with a non-existing environment', () => { + let response: Awaited>; + before(async () => { - response = await supertest.get( - url.format({ - pathname: `/internal/apm/services/opbeans-node/transactions/charts/latency`, - query: { - start, - end, - latencyAggregationType: 'avg', - transactionType: 'request', - environment: 'does-not-exist', - kuery: '', - }, - }) - ); + response = await fetchLatencyCharts({ + serviceName: 'opbeans-node', + query: { + start, + end, + latencyAggregationType: LatencyAggregationType.avg, + transactionType: 'request', + environment: 'does-not-exist', + kuery: '', + }, + }); }); it('returns average duration and timeseries', async () => { @@ -247,25 +223,23 @@ export default function ApiTest({ getService }: FtrProviderContext) { 'Transaction latency with a trial license when data is loaded', { config: 'trial', archives: [archiveName] }, () => { - let response: Awaited>; + let response: Awaited>; const transactionType = 'request'; describe('without an environment', () => { before(async () => { - response = await supertest.get( - url.format({ - pathname: `/internal/apm/services/opbeans-java/transactions/charts/latency`, - query: { - start, - end, - latencyAggregationType: 'avg', - transactionType, - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }) - ); + response = await fetchLatencyCharts({ + serviceName: 'opbeans-node', + query: { + start, + end, + latencyAggregationType: LatencyAggregationType.avg, + transactionType, + environment: 'ENVIRONMENT_ALL', + kuery: '', + }, + }); }); it('returns an ok response', () => { @@ -275,19 +249,17 @@ export default function ApiTest({ getService }: FtrProviderContext) { describe('with environment selected', () => { before(async () => { - response = await supertest.get( - url.format({ - pathname: `/internal/apm/services/opbeans-python/transactions/charts/latency`, - query: { - start, - end, - latencyAggregationType: 'avg', - transactionType, - environment: 'production', - kuery: '', - }, - }) - ); + response = await fetchLatencyCharts({ + serviceName: 'opbeans-node', + query: { + start, + end, + latencyAggregationType: LatencyAggregationType.avg, + transactionType, + environment: 'production', + kuery: '', + }, + }); }); it('should have a successful response', () => { @@ -297,19 +269,17 @@ export default function ApiTest({ getService }: FtrProviderContext) { describe('with all environments selected', () => { before(async () => { - response = await supertest.get( - url.format({ - pathname: `/internal/apm/services/opbeans-java/transactions/charts/latency`, - query: { - start, - end, - latencyAggregationType: 'avg', - transactionType, - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }) - ); + response = await fetchLatencyCharts({ + serviceName: 'opbeans-node', + query: { + start, + end, + latencyAggregationType: LatencyAggregationType.avg, + transactionType, + environment: 'ENVIRONMENT_ALL', + kuery: '', + }, + }); }); it('should have a successful response', () => { diff --git a/x-pack/test/apm_api_integration/tests/transactions/trace_samples.spec.ts b/x-pack/test/apm_api_integration/tests/transactions/trace_samples.spec.ts index 8087b781eb395..833562977f692 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/trace_samples.spec.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/trace_samples.spec.ts @@ -6,37 +6,36 @@ */ import expect from '@kbn/expect'; -import qs from 'querystring'; import { sortBy } from 'lodash'; -import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import archives from '../../common/fixtures/es_archiver/archives_metadata'; import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); - const supertest = getService('legacySupertestAsApmReadUser'); + const apmApiClient = getService('apmApiClient'); const archiveName = 'apm_8.0.0'; - const metadata = archives_metadata[archiveName]; - - const url = `/internal/apm/services/opbeans-java/transactions/traces/samples?${qs.stringify({ - environment: 'ENVIRONMENT_ALL', - kuery: '', - start: metadata.start, - end: metadata.end, - transactionName: 'APIRestController#stats', - transactionType: 'request', - })}`; + const { start, end } = archives[archiveName]; registry.when( 'Transaction trace samples response structure when data is not loaded', { config: 'basic', archives: [] }, () => { it('handles empty state', async () => { - const response: { - body: APIReturnType<'GET /internal/apm/services/{serviceName}/transactions/traces/samples'>; - status: number; - } = await supertest.get(url); + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/transactions/traces/samples', + params: { + path: { serviceName: 'opbeans-java' }, + query: { + start, + end, + transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + transactionName: 'APIRestController#stats', + kuery: '', + }, + }, + }); expect(response.status).to.be(200); @@ -49,27 +48,26 @@ export default function ApiTest({ getService }: FtrProviderContext) { 'Transaction trace samples response structure when data is loaded', { config: 'basic', archives: [archiveName] }, () => { - let response: { - body: APIReturnType<'GET /internal/apm/services/{serviceName}/transactions/traces/samples'>; - status: number; - }; + it('returns the correct samples', async () => { + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/transactions/traces/samples', + params: { + path: { serviceName: 'opbeans-java' }, + query: { + start, + end, + transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + transactionName: 'APIRestController#stats', + kuery: '', + }, + }, + }); - before(async () => { - response = await supertest.get(url); - }); + const { traceSamples } = response.body; - it('returns the correct metadata', () => { expect(response.status).to.be(200); - expect(response.body.traceSamples.length).to.be.greaterThan(0); - }); - - it('returns the correct number of samples', () => { expectSnapshot(response.body.traceSamples.length).toMatchInline(`15`); - }); - - it('returns the correct samples', () => { - const { traceSamples } = response.body; - expectSnapshot(sortBy(traceSamples, (sample) => sample.traceId)).toMatchInline(` Array [ Object { diff --git a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_main_statistics.spec.ts b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_main_statistics.spec.ts index c7e07e6b48b1b..0ec024f1c0489 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_main_statistics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_main_statistics.spec.ts @@ -7,8 +7,8 @@ import expect from '@kbn/expect'; import { pick, sum } from 'lodash'; -import url from 'url'; import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import { LatencyAggregationType } from '@kbn/apm-plugin/common/latency_aggregation_types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import archives from '../../common/fixtures/es_archiver/archives_metadata'; @@ -17,7 +17,7 @@ type TransactionsGroupsPrimaryStatistics = export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); - const supertest = getService('legacySupertestAsApmReadUser'); + const apmApiClient = getService('apmApiClient'); const archiveName = 'apm_8.0.0'; const { start, end } = archives[archiveName]; @@ -27,19 +27,20 @@ export default function ApiTest({ getService }: FtrProviderContext) { { config: 'basic', archives: [] }, () => { it('handles the empty state', async () => { - const response = await supertest.get( - url.format({ - pathname: `/internal/apm/services/opbeans-java/transactions/groups/main_statistics`, + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/transactions/groups/main_statistics', + params: { + path: { serviceName: 'opbeans-java' }, query: { start, end, - latencyAggregationType: 'avg', + latencyAggregationType: LatencyAggregationType.avg, transactionType: 'request', environment: 'ENVIRONMENT_ALL', kuery: '', }, - }) - ); + }, + }); expect(response.status).to.be(200); const transctionsGroupsPrimaryStatistics = @@ -55,19 +56,20 @@ export default function ApiTest({ getService }: FtrProviderContext) { { config: 'basic', archives: [archiveName] }, () => { it('returns the correct data', async () => { - const response = await supertest.get( - url.format({ - pathname: `/internal/apm/services/opbeans-java/transactions/groups/main_statistics`, + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/transactions/groups/main_statistics', + params: { + path: { serviceName: 'opbeans-java' }, query: { start, end, + latencyAggregationType: LatencyAggregationType.avg, transactionType: 'request', - latencyAggregationType: 'avg', environment: 'ENVIRONMENT_ALL', kuery: '', }, - }) - ); + }, + }); expect(response.status).to.be(200); @@ -130,19 +132,20 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns the correct data for latency aggregation 99th percentile', async () => { - const response = await supertest.get( - url.format({ - pathname: `/internal/apm/services/opbeans-java/transactions/groups/main_statistics`, + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/transactions/groups/main_statistics', + params: { + path: { serviceName: 'opbeans-java' }, query: { start, end, + latencyAggregationType: LatencyAggregationType.p99, transactionType: 'request', - latencyAggregationType: 'p99', environment: 'ENVIRONMENT_ALL', kuery: '', }, - }) - ); + }, + }); expect(response.status).to.be(200); From b7b9babdaf77683a707364157928f5e30de1785e Mon Sep 17 00:00:00 2001 From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com> Date: Mon, 13 Jun 2022 17:20:09 +0200 Subject: [PATCH 39/62] [Fleet] added tags to agent update api (#134201) * added tags to agent update api * added dedupe logic --- .../plugins/fleet/common/openapi/bundled.json | 23 ++++++- .../plugins/fleet/common/openapi/bundled.yaml | 13 ++++ .../openapi/paths/agents@{agent_id}.yaml | 13 ++++ .../fleet/server/routes/agent/handlers.ts | 13 +++- .../fleet/server/types/rest_spec/agent.ts | 3 +- .../apis/agents/index.js | 1 + .../apis/agents/update.ts | 64 +++++++++++++++++++ 7 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 x-pack/test/fleet_api_integration/apis/agents/update.ts diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json index 36140e0f46388..eb6f7626ea35f 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.json +++ b/x-pack/plugins/fleet/common/openapi/bundled.json @@ -1378,7 +1378,28 @@ { "$ref": "#/components/parameters/kbn_xsrf" } - ] + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "user_provided_metadata": { + "type": "object" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } }, "delete": { "summary": "Agent - Delete", diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml index 8d0d31c35cfaa..3793dfe7982dc 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.yaml +++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml @@ -854,6 +854,19 @@ paths: operationId: update-agent parameters: - $ref: '#/components/parameters/kbn_xsrf' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + user_provided_metadata: + type: object + tags: + type: array + items: + type: string delete: summary: Agent - Delete tags: [] diff --git a/x-pack/plugins/fleet/common/openapi/paths/agents@{agent_id}.yaml b/x-pack/plugins/fleet/common/openapi/paths/agents@{agent_id}.yaml index c139fe8e7e997..b5c171c6b02f5 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/agents@{agent_id}.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/agents@{agent_id}.yaml @@ -38,6 +38,19 @@ put: operationId: update-agent parameters: - $ref: ../components/headers/kbn_xsrf.yaml + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + user_provided_metadata: + type: object + tags: + type: array + items: + type: string delete: summary: Agent - Delete tags: [] diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index 15d842951e6c4..fd0a2ae4ab4bb 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { uniq } from 'lodash'; import type { RequestHandler } from '@kbn/core/server'; import type { TypeOf } from '@kbn/config-schema'; @@ -87,10 +88,16 @@ export const updateAgentHandler: RequestHandler< const coreContext = await context.core; const esClient = coreContext.elasticsearch.client.asInternalUser; + const partialAgent: any = {}; + if (request.body.user_provided_metadata) { + partialAgent.user_provided_metadata = request.body.user_provided_metadata; + } + if (request.body.tags) { + partialAgent.tags = uniq(request.body.tags); + } + try { - await AgentService.updateAgent(esClient, request.params.agentId, { - user_provided_metadata: request.body.user_provided_metadata, - }); + await AgentService.updateAgent(esClient, request.params.agentId, partialAgent); const body = { item: await AgentService.getAgentById(esClient, request.params.agentId), }; diff --git a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts index 93c67a11e2d0e..f008822437d64 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts @@ -127,7 +127,8 @@ export const UpdateAgentRequestSchema = { agentId: schema.string(), }), body: schema.object({ - user_provided_metadata: schema.recordOf(schema.string(), schema.any()), + user_provided_metadata: schema.maybe(schema.recordOf(schema.string(), schema.any())), + tags: schema.maybe(schema.arrayOf(schema.string())), }), }; diff --git a/x-pack/test/fleet_api_integration/apis/agents/index.js b/x-pack/test/fleet_api_integration/apis/agents/index.js index dbfcbf66928d9..a3ffaa59260eb 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/index.js +++ b/x-pack/test/fleet_api_integration/apis/agents/index.js @@ -15,5 +15,6 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./current_upgrades')); loadTestFile(require.resolve('./reassign')); loadTestFile(require.resolve('./status')); + loadTestFile(require.resolve('./update')); }); } diff --git a/x-pack/test/fleet_api_integration/apis/agents/update.ts b/x-pack/test/fleet_api_integration/apis/agents/update.ts new file mode 100644 index 0000000000000..830dfcff09a8f --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/agents/update.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + + describe('fleet_agents_update', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/fleet/agents'); + }); + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/fleet/agents'); + }); + + it('should return a 200 if this a valid update request with tags', async () => { + const { body: apiResponse } = await supertest + .put(`/api/fleet/agents/agent1`) + .set('kbn-xsrf', 'xx') + .send({ + tags: ['tag1'], + }) + .expect(200); + + expect(apiResponse.item.tags).to.eql(['tag1']); + }); + + it('should dedupe tags in agent update', async () => { + const { body: apiResponse } = await supertest + .put(`/api/fleet/agents/agent1`) + .set('kbn-xsrf', 'xx') + .send({ + tags: ['tag1', 'tag2', 'tag1'], + }) + .expect(200); + + expect(apiResponse.item.tags).to.eql(['tag1', 'tag2']); + }); + + it('should return a 200 if this a valid update request with user metadata', async () => { + const { body: apiResponse } = await supertest + .put(`/api/fleet/agents/agent1`) + .set('kbn-xsrf', 'xx') + .send({ + user_provided_metadata: { + data: 'test', + }, + }) + .expect(200); + + expect(apiResponse.item.user_provided_metadata).to.eql({ + data: 'test', + }); + }); + }); +} From 6ae1b710fc3ef3f04ae7428d02f7899b18b21eb5 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Mon, 13 Jun 2022 11:41:33 -0400 Subject: [PATCH 40/62] [Security Solution][Endpoint] Remove use of `devDependency` for parsing Responder console user input (#134144) - Removes use of `argsplit` (`devDependency`) and introduces new command input string parser _([new parser playground](https://codesandbox.io/s/console-command-string-parser-021057-021057?file=/src/parser.js))_ - Adds new prop to `CommandExecutionComponent` that holds a `ResultComponent` that can be used to display consistent success/failure messages - Add generic type to `CommandExecutionComponentProps` to allow for defining the Type for the command's arguments - Adjusts `isolate` and `release` commands to use new `ResultComponent` - adds `validate()` to `CommandDefinition` _(will be needed in the very near future for `kill-process`/`suspend-process` commands)_ - Several minor UI adjustments --- .../console/components/bad_argument.tsx | 69 ++++---- .../components/command_execution_output.tsx | 13 +- .../components/command_execution_result.tsx | 117 ++++++++++++ .../handle_execute_command.test.tsx | 60 +++++-- .../handle_execute_command.tsx | 88 ++++++--- .../{unknow_comand.tsx => unknown_comand.tsx} | 0 .../console/components/user_command_input.tsx | 13 +- .../management/components/console/console.tsx | 8 + .../service/parse_command_input.test.ts | 145 +++++++++++++++ .../console/service/parsed_command_input.ts | 167 ++++++++++-------- .../management/components/console/types.ts | 37 +++- .../isolate_action.test.tsx | 4 +- .../endpoint_responder/isolate_action.tsx | 41 +++-- .../release_action.test.tsx | 4 +- .../endpoint_responder/release_action.tsx | 40 ++--- .../endpoint_responder/status_action.tsx | 1 + .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 19 files changed, 607 insertions(+), 203 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/components/console/components/command_execution_result.tsx rename x-pack/plugins/security_solution/public/management/components/console/components/{unknow_comand.tsx => unknown_comand.tsx} (100%) create mode 100644 x-pack/plugins/security_solution/public/management/components/console/service/parse_command_input.test.ts diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/bad_argument.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/bad_argument.tsx index 199b30a928dc6..5966e4a0e0e3c 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/bad_argument.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/bad_argument.tsx @@ -5,18 +5,18 @@ * 2.0. */ -import React, { memo, PropsWithChildren, useEffect } from 'react'; +import React, { memo, PropsWithChildren, ReactNode, useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiCode, EuiText } from '@elastic/eui'; import { UnsupportedMessageCallout } from './unsupported_message_callout'; -import { ParsedCommandInput } from '../service/parsed_command_input'; +import { ParsedCommandInterface } from '../service/parsed_command_input'; import { CommandDefinition, CommandExecutionComponentProps } from '../types'; import { CommandInputUsage } from './command_usage'; import { useDataTestSubj } from '../hooks/state_selectors/use_data_test_subj'; import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; export type BadArgumentProps = PropsWithChildren<{ - parsedInput: ParsedCommandInput; + parsedInput: ParsedCommandInterface; commandDefinition: CommandDefinition; }>; @@ -24,37 +24,42 @@ export type BadArgumentProps = PropsWithChildren<{ * Shows a bad argument error. The error message needs to be defined via the Command History Item's * `state.errorMessage` */ -export const BadArgument = memo(({ command, setStatus, store }) => { - const getTestId = useTestIdGenerator(useDataTestSubj()); +export const BadArgument = memo>( + ({ command, setStatus, store }) => { + const getTestId = useTestIdGenerator(useDataTestSubj()); - useEffect(() => { - setStatus('success'); - }, [setStatus]); + useEffect(() => { + setStatus('success'); + }, [setStatus]); - return ( - - } - data-test-subj={getTestId('badArgument')} - > - <> - {store.errorMessage} - - + return ( + {`${command.commandDefinition.name} --help`}, - }} + id="xpack.securitySolution.console.badArgument.title" + defaultMessage="Unsupported argument!" /> - - - - ); -}); + } + data-test-subj={getTestId('badArgument')} + > + <> +
    {store.errorMessage}
    +
    + +
    + {'. '} + + {`${command.commandDefinition.name} --help`}, + }} + /> + + + + ); + } +); BadArgument.displayName = 'BadArgument'; diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/command_execution_output.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/command_execution_output.tsx index 53af10bb06323..61dd798a11d65 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/command_execution_output.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/command_execution_output.tsx @@ -6,8 +6,9 @@ */ import React, { memo, useCallback, useMemo } from 'react'; -import { EuiLoadingChart } from '@elastic/eui'; +import { EuiLoadingChart, EuiSpacer } from '@elastic/eui'; import styled from 'styled-components'; +import { CommandExecutionResult } from './command_execution_result'; import type { CommandExecutionComponentProps } from '../types'; import type { CommandExecutionState, CommandHistoryItem } from './console_state/types'; import { UserCommandInput } from './user_command_input'; @@ -15,6 +16,10 @@ import { useConsoleStateDispatch } from '../hooks/state_selectors/use_console_st const CommandOutputContainer = styled.div` position: relative; + + .busy-indicator { + margin-right: 1em; + } `; export interface CommandExecutionOutputProps { @@ -61,15 +66,19 @@ export const CommandExecutionOutput = memo(
    - {isRunning && }
    + + + {isRunning && } +
    diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/command_execution_result.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/command_execution_result.tsx new file mode 100644 index 0000000000000..c5ff02cefaf70 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/console/components/command_execution_result.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, PropsWithChildren, ComponentType, useMemo } from 'react'; +import type { ReactNode } from 'react'; +import { i18n } from '@kbn/i18n'; +import { CommonProps, EuiPanel, EuiSpacer, EuiText, EuiTextColor } from '@elastic/eui'; +import classNames from 'classnames'; +import { useDataTestSubj } from '../hooks/state_selectors/use_data_test_subj'; +import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; + +const COMMAND_EXECUTION_RESULT_SUCCESS_TITLE = i18n.translate( + 'xpack.securitySolution.commandExecutionResult.successTitle', + { defaultMessage: 'Command successful.' } +); +const COMMAND_EXECUTION_RESULT_FAILURE_TITLE = i18n.translate( + 'xpack.securitySolution.commandExecutionResult.failureTitle', + { defaultMessage: 'Command failed.' } +); +const COMMAND_EXECUTION_RESULT_PENDING = i18n.translate( + 'xpack.securitySolution.commandExecutionResult.pending', + { defaultMessage: 'Waiting for response' } +); + +export type CommandExecutionResultProps = PropsWithChildren<{ + /** + * Default is `success`. + * + * **IMPORTANT**: Note that when `pending` is used, the `title` will NOT be shown - only `children`. + * Also, + * The element output to DOM will be `inline-block`, allowing for messages + * to be displayed after the loading/busy indicator. + */ + showAs?: 'success' | 'failure' | 'pending'; + + /** Default title message are provided depending based on the value for `showAs` */ + title?: ReactNode; + + /** If the title should be shown. Default is true */ + showTitle?: boolean; + + className?: CommonProps['className']; + + 'data-test-subj'?: string; +}>; + +/** + * A component that can be used by consumers of the Console to format the result of a command. + * Applies consistent structure, colors and formatting, and includes ability to set a title and + * whether the result is a success or failure. + */ +export const CommandExecutionResult = memo( + ({ + showAs = 'success', + title, + showTitle = true, + 'data-test-subj': dataTestSubj, + className, + children, + }) => { + const consoleDataTestSubj = useDataTestSubj(); + const getTestId = useTestIdGenerator(dataTestSubj ?? consoleDataTestSubj); + + const panelClassName = useMemo(() => { + return classNames({ + 'eui-displayInlineBlock': showAs === 'pending', + // This class name (font-family-code) is a utility class defined in `Console.tsx` + 'font-family-code': true, + [className || '_']: Boolean(className), + }); + }, [className, showAs]); + + return ( + + {showAs === 'pending' ? ( + + + {children ?? COMMAND_EXECUTION_RESULT_PENDING} + + + ) : ( + <> + {showTitle && ( + <> + + + {title + ? title + : showAs === 'success' + ? COMMAND_EXECUTION_RESULT_SUCCESS_TITLE + : COMMAND_EXECUTION_RESULT_FAILURE_TITLE} + + + + + )} + {children} + + )} + + ); + } +); +CommandExecutionResult.displayName = 'CommandExecutionResult'; + +export type CommandExecutionResultComponent = ComponentType; diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.test.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.test.tsx index 6d24dba923008..5de422a819227 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.test.tsx @@ -120,19 +120,30 @@ describe('When a Console command is entered by the user', () => { enterCommand('cmd1 --foo'); await waitFor(() => { - expect(renderResult.getByTestId('test-badArgument').textContent).toEqual( - 'Unsupported argument!command does not support any argumentsUsage:cmd1Type cmd1 --help for assistance.' + expect(renderResult.getByTestId('test-badArgument-message').textContent).toEqual( + 'Command does not support any arguments' ); }); }); - it('should show error if unknown option is used', async () => { + it('should show error if unknown (single) argument is used', async () => { render(); enterCommand('cmd2 --file test --foo'); await waitFor(() => { - expect(renderResult.getByTestId('test-badArgument').textContent).toEqual( - 'Unsupported argument!unsupported argument: --fooUsage:cmd2--file [--ext --bad]Type cmd2 --help for assistance.' + expect(renderResult.getByTestId('test-badArgument-message').textContent).toEqual( + 'The following cmd2 argument is not support by this command: --foo' + ); + }); + }); + + it('should show error if unknown (multiple) arguments are used', async () => { + render(); + enterCommand('cmd2 --file test --foo --bar'); + + await waitFor(() => { + expect(renderResult.getByTestId('test-badArgument-message').textContent).toEqual( + 'The following cmd2 arguments are not support by this command: --foo, --bar' ); }); }); @@ -142,8 +153,8 @@ describe('When a Console command is entered by the user', () => { enterCommand('cmd2 --ext one'); await waitFor(() => { - expect(renderResult.getByTestId('test-badArgument').textContent).toEqual( - 'Unsupported argument!missing required argument: --fileUsage:cmd2--file [--ext --bad]Type cmd2 --help for assistance.' + expect(renderResult.getByTestId('test-badArgument-message').textContent).toEqual( + 'Missing required argument: --file' ); }); }); @@ -153,8 +164,8 @@ describe('When a Console command is entered by the user', () => { enterCommand('cmd2 --file one --file two'); await waitFor(() => { - expect(renderResult.getByTestId('test-badArgument').textContent).toEqual( - 'Unsupported argument!argument can only be used once: --fileUsage:cmd2--file [--ext --bad]Type cmd2 --help for assistance.' + expect(renderResult.getByTestId('test-badArgument-message').textContent).toEqual( + 'Argument can only be used once: --file' ); }); }); @@ -164,8 +175,8 @@ describe('When a Console command is entered by the user', () => { enterCommand('cmd2 --file one --bad foo'); await waitFor(() => { - expect(renderResult.getByTestId('test-badArgument').textContent).toEqual( - 'Unsupported argument!invalid argument value: --bad. This is a bad valueUsage:cmd2--file [--ext --bad]Type cmd2 --help for assistance.' + expect(renderResult.getByTestId('test-badArgument-message').textContent).toEqual( + 'Invalid argument value: --bad. This is a bad value' ); }); }); @@ -175,8 +186,8 @@ describe('When a Console command is entered by the user', () => { enterCommand('cmd2'); await waitFor(() => { - expect(renderResult.getByTestId('test-badArgument').textContent).toEqual( - 'Unsupported argument!missing required arguments: --fileUsage:cmd2--file [--ext --bad]Type cmd2 --help for assistance.' + expect(renderResult.getByTestId('test-badArgument-message').textContent).toEqual( + 'Missing required arguments: --file' ); }); }); @@ -186,8 +197,27 @@ describe('When a Console command is entered by the user', () => { enterCommand('cmd4'); await waitFor(() => { - expect(renderResult.getByTestId('test-badArgument').textContent).toEqual( - 'Unsupported argument!at least one argument must be usedUsage:cmd4[--foo --bar]Type cmd4 --help for assistance.' + expect(renderResult.getByTestId('test-badArgument-message').textContent).toEqual( + 'At least one argument must be used' + ); + }); + }); + + it('should show error if command definition `validate()` callback return a message', async () => { + const cmd1Definition = commands.find((command) => command.name === 'cmd1'); + + if (!cmd1Definition) { + throw new Error('cmd1 defintion not fount'); + } + + cmd1Definition.validate = () => 'command is invalid'; + + render(); + enterCommand('cmd1'); + + await waitFor(() => { + expect(renderResult.getByTestId('test-badArgument-message').textContent).toEqual( + 'command is invalid' ); }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.tsx index c387cf3d90a8f..42e1548ff25a0 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.tsx @@ -5,8 +5,14 @@ * 2.0. */ +// FIXME:PT breakup module in order to avoid turning off eslint rule below +/* eslint-disable complexity */ + import { i18n } from '@kbn/i18n'; import { v4 as uuidV4 } from 'uuid'; +import { EuiCode } from '@elastic/eui'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; import { HelpCommandArgument } from '../../builtin_commands/help_command_argument'; import { CommandHistoryItem, @@ -14,8 +20,8 @@ import { ConsoleDataState, ConsoleStoreReducer, } from '../types'; -import { parseCommandInput } from '../../../service/parsed_command_input'; -import { UnknownCommand } from '../../unknow_comand'; +import { parseCommandInput, ParsedCommandInterface } from '../../../service/parsed_command_input'; +import { UnknownCommand } from '../../unknown_comand'; import { BadArgument } from '../../bad_argument'; import { Command, CommandDefinition, CommandExecutionComponentProps } from '../../../types'; @@ -31,6 +37,21 @@ const getRequiredArguments = (argDefinitions: CommandDefinition['args']): string .map(([argName]) => argName); }; +const getUnknownArguments = ( + inputArgs: ParsedCommandInterface['args'], + argDefinitions: CommandDefinition['args'] | undefined +): string[] => { + const response: string[] = []; + + Object.keys(inputArgs).forEach((argName) => { + if (!argDefinitions || !argDefinitions[argName]) { + response.push(argName); + } + }); + + return response; +}; + const updateStateWithNewCommandHistoryItem = ( state: ConsoleDataState, newHistoryItem: ConsoleDataState['commandHistory'][number] @@ -109,7 +130,7 @@ export const handleExecuteCommand: ConsoleStoreReducer< const requiredArgs = getRequiredArguments(commandDefinition.args); // If args were entered, then validate them - if (parsedInput.hasArgs()) { + if (parsedInput.hasArgs) { // Show command help if (parsedInput.hasArg('help')) { return updateStateWithNewCommandHistoryItem(state, { @@ -128,7 +149,7 @@ export const handleExecuteCommand: ConsoleStoreReducer< errorMessage: i18n.translate( 'xpack.securitySolution.console.commandValidation.noArgumentsSupported', { - defaultMessage: 'command does not support any arguments', + defaultMessage: 'Command does not support any arguments', } ), }), @@ -136,19 +157,27 @@ export const handleExecuteCommand: ConsoleStoreReducer< } // no unknown arguments allowed? - if (parsedInput.unknownArgs && parsedInput.unknownArgs.length) { + const unknownInputArgs = getUnknownArguments(parsedInput.args, commandDefinition.args); + + if (unknownInputArgs.length) { return updateStateWithNewCommandHistoryItem(state, { id: uuidV4(), command: cloneCommandDefinitionWithNewRenderComponent(command, BadArgument), state: createCommandExecutionState({ - errorMessage: i18n.translate( - 'xpack.securitySolution.console.commandValidation.unknownArgument', - { - defaultMessage: 'unknown argument(s): {unknownArgs}', - values: { - unknownArgs: parsedInput.unknownArgs.join(', '), - }, - } + errorMessage: ( + {parsedInput.name}, + unknownArgs: ( + + {unknownInputArgs.map(toCliArgumentOption).join(', ')} + + ), + }} + /> ), }), }); @@ -164,7 +193,7 @@ export const handleExecuteCommand: ConsoleStoreReducer< errorMessage: i18n.translate( 'xpack.securitySolution.console.commandValidation.missingRequiredArg', { - defaultMessage: 'missing required argument: {argName}', + defaultMessage: 'Missing required argument: {argName}', values: { argName: toCliArgumentOption(requiredArg), }, @@ -189,7 +218,7 @@ export const handleExecuteCommand: ConsoleStoreReducer< errorMessage: i18n.translate( 'xpack.securitySolution.console.commandValidation.unsupportedArg', { - defaultMessage: 'unsupported argument: {argName}', + defaultMessage: 'Unsupported argument: {argName}', values: { argName: toCliArgumentOption(argName) }, } ), @@ -198,11 +227,7 @@ export const handleExecuteCommand: ConsoleStoreReducer< } // does not allow multiple values - if ( - !argDefinition.allowMultiples && - Array.isArray(argInput.values) && - argInput.values.length > 0 - ) { + if (!argDefinition.allowMultiples && Array.isArray(argInput) && argInput.length > 1) { return updateStateWithNewCommandHistoryItem(state, { id: uuidV4(), command: cloneCommandDefinitionWithNewRenderComponent(command, BadArgument), @@ -210,7 +235,7 @@ export const handleExecuteCommand: ConsoleStoreReducer< errorMessage: i18n.translate( 'xpack.securitySolution.console.commandValidation.argSupportedOnlyOnce', { - defaultMessage: 'argument can only be used once: {argName}', + defaultMessage: 'Argument can only be used once: {argName}', values: { argName: toCliArgumentOption(argName) }, } ), @@ -229,7 +254,7 @@ export const handleExecuteCommand: ConsoleStoreReducer< errorMessage: i18n.translate( 'xpack.securitySolution.console.commandValidation.invalidArgValue', { - defaultMessage: 'invalid argument value: {argName}. {error}', + defaultMessage: 'Invalid argument value: {argName}. {error}', values: { argName: toCliArgumentOption(argName), error: validationResult }, } ), @@ -246,7 +271,7 @@ export const handleExecuteCommand: ConsoleStoreReducer< errorMessage: i18n.translate( 'xpack.securitySolution.console.commandValidation.mustHaveArgs', { - defaultMessage: 'missing required arguments: {requiredArgs}', + defaultMessage: 'Missing required arguments: {requiredArgs}', values: { requiredArgs: requiredArgs.map((argName) => toCliArgumentOption(argName)).join(', '), }, @@ -262,13 +287,28 @@ export const handleExecuteCommand: ConsoleStoreReducer< errorMessage: i18n.translate( 'xpack.securitySolution.console.commandValidation.oneArgIsRequired', { - defaultMessage: 'at least one argument must be used', + defaultMessage: 'At least one argument must be used', } ), }), }); } + // if the Command definition has a `validate()` callback, then call it now + if (commandDefinition.validate) { + const validationResult = commandDefinition.validate(command); + + if (validationResult !== true) { + return updateStateWithNewCommandHistoryItem(state, { + id: uuidV4(), + command: cloneCommandDefinitionWithNewRenderComponent(command, BadArgument), + state: createCommandExecutionState({ + errorMessage: validationResult, + }), + }); + } + } + // All is good. Execute the command return updateStateWithNewCommandHistoryItem(state, { id: uuidV4(), diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/unknow_comand.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/unknown_comand.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/management/components/console/components/unknow_comand.tsx rename to x-pack/plugins/security_solution/public/management/components/console/components/unknown_comand.tsx diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/user_command_input.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/user_command_input.tsx index 84afff3f28209..cebbb031b1229 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/user_command_input.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/user_command_input.tsx @@ -6,6 +6,12 @@ */ import React, { memo } from 'react'; +import { EuiCode } from '@elastic/eui'; +import styled from 'styled-components'; + +const StyledEuiCode = styled(EuiCode)` + padding-left: 0; +`; export interface UserCommandInputProps { input: string; @@ -13,10 +19,9 @@ export interface UserCommandInputProps { export const UserCommandInput = memo(({ input }) => { return ( - <> - {'$ '} - {input} - + + {input} + ); }); UserCommandInput.displayName = 'UserCommandInput'; diff --git a/x-pack/plugins/security_solution/public/management/components/console/console.tsx b/x-pack/plugins/security_solution/public/management/components/console/console.tsx index 2075c4f30260e..3306870fea948 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/console.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/console.tsx @@ -65,6 +65,14 @@ const ConsoleWindow = styled.div` } } + //----------------------------------------------------------- + // 👇 Utility classnames for use anywhere inside of Console + //----------------------------------------------------------- + + .font-family-code { + font-family: ${({ theme: { eui } }) => eui.euiCodeFontFamily}; + } + .descriptionList-20_80 { &.euiDescriptionList { > .euiDescriptionList__title { diff --git a/x-pack/plugins/security_solution/public/management/components/console/service/parse_command_input.test.ts b/x-pack/plugins/security_solution/public/management/components/console/service/parse_command_input.test.ts new file mode 100644 index 0000000000000..ae463ac44a49b --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/console/service/parse_command_input.test.ts @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { parseCommandInput, ParsedCommandInterface } from './parsed_command_input'; + +describe('when using `parseCommandInput()`', () => { + const parsedCommandWith = ( + overrides: Partial> = {} + ): ParsedCommandInterface => { + return { + input: '', + name: 'foo', + args: {}, + hasArgs: Object.keys(overrides.args || {}).length > 0, + ...overrides, + } as ParsedCommandInterface; + }; + + it.each([ + ['foo', 'foo'], + [' foo', 'foo'], + [' foo ', 'foo'], + ['foo ', 'foo'], + [' foo-bar ', 'foo-bar'], + ])('should identify the command entered: [%s]', (input, expectedCommandName) => { + const parsedCommand = parseCommandInput(input); + + expect(parsedCommand).toEqual( + parsedCommandWith({ + input, + name: expectedCommandName, + args: {}, + }) + ); + }); + + it('should parse arguments that the `--` prefix', () => { + const input = 'foo --one --two'; + const parsedCommand = parseCommandInput(input); + + expect(parsedCommand).toEqual( + parsedCommandWith({ + input, + args: { + one: [], + two: [], + }, + }) + ); + }); + + it('should parse arguments that have a single string value', () => { + const input = 'foo --one value --two=value2'; + const parsedCommand = parseCommandInput(input); + + expect(parsedCommand).toEqual( + parsedCommandWith({ + input, + args: { + one: ['value'], + two: ['value2'], + }, + }) + ); + }); + + it('should parse arguments that have multiple strings as the value', () => { + const input = 'foo --one value for one here --two=some more strings for 2'; + const parsedCommand = parseCommandInput(input); + + expect(parsedCommand).toEqual( + parsedCommandWith({ + input, + args: { + one: ['value for one here'], + two: ['some more strings for 2'], + }, + }) + ); + }); + + it('should parse arguments whose value is wrapped in quotes', () => { + const input = 'foo --one "value for one here" --two="some more strings for 2"'; + const parsedCommand = parseCommandInput(input); + + expect(parsedCommand).toEqual( + parsedCommandWith({ + input, + args: { + one: ['value for one here'], + two: ['some more strings for 2'], + }, + }) + ); + }); + + it('should parse arguments that can be used multiple times', () => { + const input = 'foo --one 1 --one 11 --two=2 --two=22'; + const parsedCommand = parseCommandInput(input); + + expect(parsedCommand).toEqual( + parsedCommandWith({ + input, + args: { + one: ['1', '11'], + two: ['2', '22'], + }, + }) + ); + }); + + it('should parse arguments whose value has `--` in it (must be escaped)', () => { + const input = 'foo --one something \\-\\- here --two="\\-\\-something \\-\\-'; + const parsedCommand = parseCommandInput(input); + + expect(parsedCommand).toEqual( + parsedCommandWith({ + input, + args: { + one: ['something -- here'], + two: ['--something --'], + }, + }) + ); + }); + + it('should parse arguments whose value has `=` in it', () => { + const input = 'foo --one =something \\-\\- here --two="=something=something else'; + const parsedCommand = parseCommandInput(input); + + expect(parsedCommand).toEqual( + parsedCommandWith({ + input, + args: { + one: ['=something -- here'], + two: ['=something=something else'], + }, + }) + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/components/console/service/parsed_command_input.ts b/x-pack/plugins/security_solution/public/management/components/console/service/parsed_command_input.ts index 55e0b3dc6267b..2e1564edb22e6 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/service/parsed_command_input.ts +++ b/x-pack/plugins/security_solution/public/management/components/console/service/parsed_command_input.ts @@ -5,91 +5,114 @@ * 2.0. */ -// @ts-ignore -// eslint-disable-next-line import/no-extraneous-dependencies -import argsplit from 'argsplit'; +/* eslint-disable @typescript-eslint/no-explicit-any */ -// FIXME:PT use a 3rd party lib for arguments parsing -// For now, just using what I found in kibana package.json devDependencies, so this will NOT work for production +export type ParsedArgData = string[]; -// FIXME:PT Type `ParsedCommandInput` should be a generic that allows for the args's keys to be defined - -export interface ParsedArgData { - /** For arguments that were used only once. Will be `undefined` if multiples were used */ - value: undefined | string; - /** For arguments that were used multiple times */ - values: undefined | string[]; -} - -export interface ParsedCommandInput { - input: string; +interface ParsedCommandInput { name: string; args: { - [argName: string]: ParsedArgData; + [key in keyof TArgs]: ParsedArgData; }; - unknownArgs: undefined | string[]; - hasArgs(): boolean; - hasArg(argName: string): boolean; } +const parseInputString = (rawInput: string): ParsedCommandInput => { + const input = rawInput.trim(); + const inputFirstSpacePosition = input.indexOf(' '); -const PARSED_COMMAND_INPUT_PROTOTYPE: Pick = Object.freeze({ - hasArgs(this: ParsedCommandInput) { - return Object.keys(this.args).length > 0 || Array.isArray(this.unknownArgs); - }, + const response: ParsedCommandInput = { + name: input.substring( + 0, + inputFirstSpacePosition === -1 ? input.length : inputFirstSpacePosition + ), + args: {}, + }; - hasArg(argName: string): boolean { - // @ts-ignore - return Object.prototype.hasOwnProperty.call(this.args, argName); - }, -}); - -export const parseCommandInput = (input: string): ParsedCommandInput => { - const inputTokens: string[] = argsplit(input) || []; - const name: string = inputTokens.shift() || ''; - const args: ParsedCommandInput['args'] = {}; - let unknownArgs: ParsedCommandInput['unknownArgs']; - - // All options start with `--` - let argName = ''; - - for (const inputToken of inputTokens) { - if (inputToken.startsWith('--')) { - argName = inputToken.substr(2); - - if (!args[argName]) { - args[argName] = { - value: undefined, - values: undefined, - }; - } + const rawArguments = + inputFirstSpacePosition === -1 + ? [] + : input.substring(inputFirstSpacePosition).trim().split(/--/); - // eslint-disable-next-line no-continue - continue; - } else if (!argName) { - (unknownArgs = unknownArgs || []).push(inputToken); + for (const rawArg of rawArguments) { + const argNameAndValueTrimmedString = rawArg.trim(); - // eslint-disable-next-line no-continue - continue; - } + if (argNameAndValueTrimmedString) { + // rawArgument possible values here are: + // 'option=something' + // 'option' + // 'option something + // These all having possible spaces before and after + + const firstSpaceOrEqualSign = /[ =]/.exec(argNameAndValueTrimmedString); + + // Grab the argument name + const argName = ( + firstSpaceOrEqualSign + ? argNameAndValueTrimmedString.substring(0, firstSpaceOrEqualSign.index).trim() + : argNameAndValueTrimmedString + ).trim(); + + if (argName) { + if (!response.args[argName]) { + response.args[argName] = []; + } + + // if this argument name as a value, then process that + if (argName !== argNameAndValueTrimmedString && firstSpaceOrEqualSign) { + let newArgValue = argNameAndValueTrimmedString + .substring(firstSpaceOrEqualSign.index + 1) + .trim() + .replace(/\\/g, ''); + + if (newArgValue.charAt(0) === '"') { + newArgValue = newArgValue.substring(1); + } + + if (newArgValue.charAt(newArgValue.length - 1) === '"') { + newArgValue = newArgValue.substring(0, newArgValue.length - 1); + } - if (Array.isArray(args[argName].values)) { - // @ts-ignore - args[argName].values.push(inputToken); - } else { - // Do we have multiple values for this argumentName, then create array for values - if (args[argName].value !== undefined) { - args[argName].values = [args[argName].value ?? '', inputToken]; - args[argName].value = undefined; - } else { - args[argName].value = inputToken; + response.args[argName].push(newArgValue); + } } } } - return Object.assign(Object.create(PARSED_COMMAND_INPUT_PROTOTYPE), { - input, - name, - args, - unknownArgs, - }); + return response; +}; + +export interface ParsedCommandInterface + extends ParsedCommandInput { + input: string; + + /** + * Checks if the given argument name was entered by the user + * @param argName + */ + hasArg(argName: string): boolean; + + /** + * if any argument was entered + */ + hasArgs: boolean; +} + +class ParsedCommand implements ParsedCommandInterface { + public readonly name: string; + public readonly args: Record; + public readonly hasArgs: boolean; + + constructor(public readonly input: string) { + const parseInput = parseInputString(input); + this.name = parseInput.name; + this.args = parseInput.args; + this.hasArgs = Object.keys(this.args).length > 0; + } + + hasArg(argName: string): boolean { + return argName in this.args; + } +} + +export const parseCommandInput = (input: string): ParsedCommandInterface => { + return new ParsedCommand(input); }; diff --git a/x-pack/plugins/security_solution/public/management/components/console/types.ts b/x-pack/plugins/security_solution/public/management/components/console/types.ts index a9c5680074705..de2ec38b16015 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/types.ts +++ b/x-pack/plugins/security_solution/public/management/components/console/types.ts @@ -9,9 +9,10 @@ import type { ComponentType } from 'react'; import type { CommonProps } from '@elastic/eui'; +import { CommandExecutionResultComponent } from './components/command_execution_result'; import type { CommandExecutionState } from './components/console_state/types'; import type { Immutable, MaybeImmutable } from '../../../../common/endpoint/types'; -import type { ParsedArgData, ParsedCommandInput } from './service/parsed_command_input'; +import type { ParsedArgData, ParsedCommandInterface } from './service/parsed_command_input'; export interface CommandDefinition { name: string; @@ -34,6 +35,15 @@ export interface CommandDefinition { /** If all args are optional, but at least one must be defined, set to true */ mustHaveArgs?: boolean; + + /** + * Validate the command entered by the user. This is called only after the Console has ran + * through all of its builtin validations (based on `CommandDefinition`). + * Example: used it when there are multiple optional arguments but at least one of those + * must be defined. + */ + validate?: (command: Command) => true | string; + /** The list of arguments supported by this command */ args?: { [longName: string]: { @@ -45,6 +55,7 @@ export interface CommandDefinition { * Should return `true` if valid or a string with the error message */ validate?: (argData: ParsedArgData) => true | string; + // Selector: Idea is that the schema can plugin in a rich component for the // user to select something (ex. a file) // FIXME: implement selector @@ -56,21 +67,28 @@ export interface CommandDefinition { /** * A command to be executed (as entered by the user) */ -export interface Command { +export interface Command< + TDefinition extends CommandDefinition = CommandDefinition, + TArgs extends object = any +> { /** The raw input entered by the user */ input: string; // FIXME:PT this should be a generic that allows for the arguments type to be used /** An object with the arguments entered by the user and their value */ - args: ParsedCommandInput; + args: ParsedCommandInterface; /** The command definition associated with this user command */ commandDefinition: TDefinition; } export interface CommandExecutionComponentProps< + /** The arguments that could have been entered by the user */ + TArgs extends object = any, + /** Internal store for the Command execution */ TStore extends object = Record, + /** The metadata defined on the Command Definition */ TMeta = any > { - command: Command>; + command: Command, TArgs>; /** * A data store for the command execution to store data in, if needed. @@ -98,15 +116,24 @@ export interface CommandExecutionComponentProps< /** Set the status of the command execution */ setStatus: (status: CommandExecutionState['status']) => void; + + /** + * A component that can be used to format the returned result from the command execution. + */ + ResultComponent: CommandExecutionResultComponent; } /** * The component that will handle the Command execution and display the result. */ export type CommandExecutionComponent< + /** The arguments that could have been entered by the user */ + TArgs extends object = any, + /** Internal store for the Command execution */ TStore extends object = Record, + /** The metadata defined on the Command Definition */ TMeta = any -> = ComponentType>; +> = ComponentType>; export interface ConsoleProps extends CommonProps { /** diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/isolate_action.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/isolate_action.test.tsx index 369bf7e451fef..b3fc1b7335012 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/isolate_action.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/isolate_action.test.tsx @@ -78,8 +78,8 @@ describe('When using isolate action from response actions console', () => { await render(); enterConsoleCommand(renderResult, 'isolate --comment "one" --comment "two"'); - expect(renderResult.getByTestId('test-badArgument').textContent).toMatch( - /argument can only be used once: --comment/ + expect(renderResult.getByTestId('test-badArgument-message').textContent).toEqual( + 'Argument can only be used once: --comment' ); }); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/isolate_action.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/isolate_action.tsx index c83620bcb9135..7ce6238044597 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/isolate_action.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/isolate_action.tsx @@ -8,7 +8,6 @@ import React, { memo, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiCallOut } from '@elastic/eui'; import { ActionDetails } from '../../../../common/endpoint/types'; import { useGetActionDetails } from '../../hooks/endpoint/use_get_action_details'; import { EndpointCommandDefinitionMeta } from './types'; @@ -17,6 +16,7 @@ import { CommandExecutionComponentProps } from '../console/types'; export const IsolateActionResult = memo< CommandExecutionComponentProps< + { comment?: string }, { actionId?: string; actionRequestSent?: boolean; @@ -24,7 +24,7 @@ export const IsolateActionResult = memo< }, EndpointCommandDefinitionMeta > ->(({ command, setStore, store, status, setStatus }) => { +>(({ command, setStore, store, status, setStatus, ResultComponent }) => { const endpointId = command.commandDefinition?.meta?.endpointId; const { actionId, completedActionDetails } = store; const isPending = status === 'pending'; @@ -42,14 +42,14 @@ export const IsolateActionResult = memo< if (!actionRequestSent && endpointId) { isolateHostApi.mutate({ endpoint_ids: [endpointId], - comment: command.args.args?.comment?.value, + comment: command.args.args?.comment?.[0], }); setStore((prevState) => { return { ...prevState, actionRequestSent: true }; }); } - }, [actionRequestSent, command.args.args?.comment?.value, endpointId, isolateHostApi, setStore]); + }, [actionRequestSent, command.args.args?.comment, endpointId, isolateHostApi, setStore]); // If isolate request was created, store the action id if necessary useEffect(() => { @@ -74,46 +74,45 @@ export const IsolateActionResult = memo< // Show nothing if still pending if (isPending) { - return null; + return ( + + + + ); } // Show errors if (completedActionDetails?.errors) { return ( - - + ); } // Show Success return ( - - - + /> ); }); IsolateActionResult.displayName = 'IsolateActionResult'; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/release_action.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/release_action.test.tsx index 1471010249f45..336ee9d5bd8f8 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/release_action.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/release_action.test.tsx @@ -79,8 +79,8 @@ describe('When using the release action from response actions console', () => { await render(); enterConsoleCommand(renderResult, 'release --comment "one" --comment "two"'); - expect(renderResult.getByTestId('test-badArgument').textContent).toMatch( - /argument can only be used once: --comment/ + expect(renderResult.getByTestId('test-badArgument-message').textContent).toEqual( + 'Argument can only be used once: --comment' ); }); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/release_action.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/release_action.tsx index 3e2ae27ffbf09..65de5580ed90f 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/release_action.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/release_action.tsx @@ -8,7 +8,6 @@ import React, { memo, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiCallOut } from '@elastic/eui'; import { ActionDetails } from '../../../../common/endpoint/types'; import { useGetActionDetails } from '../../hooks/endpoint/use_get_action_details'; import { EndpointCommandDefinitionMeta } from './types'; @@ -17,6 +16,7 @@ import { CommandExecutionComponentProps } from '../console/types'; export const ReleaseActionResult = memo< CommandExecutionComponentProps< + { comment?: string }, { actionId?: string; actionRequestSent?: boolean; @@ -24,7 +24,7 @@ export const ReleaseActionResult = memo< }, EndpointCommandDefinitionMeta > ->(({ command, setStore, store, status, setStatus }) => { +>(({ command, setStore, store, status, setStatus, ResultComponent }) => { const endpointId = command.commandDefinition?.meta?.endpointId; const { actionId, completedActionDetails } = store; const isPending = status === 'pending'; @@ -42,14 +42,14 @@ export const ReleaseActionResult = memo< if (!actionRequestSent && endpointId) { releaseHostApi.mutate({ endpoint_ids: [endpointId], - comment: command.args.args?.comment?.value, + comment: command.args.args?.comment?.[0], }); setStore((prevState) => { return { ...prevState, actionRequestSent: true }; }); } - }, [actionRequestSent, command.args.args?.comment?.value, endpointId, releaseHostApi, setStore]); + }, [actionRequestSent, command.args.args?.comment, endpointId, releaseHostApi, setStore]); // If release request was created, store the action id if necessary useEffect(() => { @@ -74,46 +74,44 @@ export const ReleaseActionResult = memo< // Show nothing if still pending if (isPending) { - return null; + return ( + + + + ); } // Show errors if (completedActionDetails?.errors) { return ( - - + ); } // Show Success return ( - - - + /> ); }); ReleaseActionResult.displayName = 'ReleaseActionResult'; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/status_action.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/status_action.tsx index fd59a3e82063f..9b907738fb504 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/status_action.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/status_action.tsx @@ -24,6 +24,7 @@ import { FormattedError } from '../formatted_error'; export const EndpointStatusActionResult = memo< CommandExecutionComponentProps< + {}, { apiCalled?: boolean; endpointDetails?: HostInfo; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 7436341ea8e2d..3224d068f0c0a 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -23767,7 +23767,6 @@ "xpack.securitySolution.console.commandValidation.mustHaveArgs": "arguments requis manquants : {requiredArgs}", "xpack.securitySolution.console.commandValidation.noArgumentsSupported": "cette commande ne prend pas en charge les arguments.", "xpack.securitySolution.console.commandValidation.oneArgIsRequired": "au moins un argument doit être utilisé.", - "xpack.securitySolution.console.commandValidation.unknownArgument": "argument(s) inconnu(s) : {unknownArgs}", "xpack.securitySolution.console.commandValidation.unsupportedArg": "argument non pris en charge : {argName}", "xpack.securitySolution.console.unknownCommand.title": "Commande inconnue", "xpack.securitySolution.containers.anomalies.errorFetchingAnomaliesData": "Impossible d'interroger les données d'anomalies", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d40657fbd01bd..8d1b132e9e9f6 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -23902,7 +23902,6 @@ "xpack.securitySolution.console.commandValidation.mustHaveArgs": "不足している必須の引数:{requiredArgs}", "xpack.securitySolution.console.commandValidation.noArgumentsSupported": "コマンドは引数をサポートしていません", "xpack.securitySolution.console.commandValidation.oneArgIsRequired": "1つ以上の引数を使用する必要があります", - "xpack.securitySolution.console.commandValidation.unknownArgument": "不明な引数:{unknownArgs}", "xpack.securitySolution.console.commandValidation.unsupportedArg": "サポートされていない引数:{argName}", "xpack.securitySolution.console.unknownCommand.title": "不明なコマンド", "xpack.securitySolution.containers.anomalies.errorFetchingAnomaliesData": "異常データをクエリできませんでした", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d691c54302eae..e561dcd17fb03 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -23934,7 +23934,6 @@ "xpack.securitySolution.console.commandValidation.mustHaveArgs": "缺少所需参数:{requiredArgs}", "xpack.securitySolution.console.commandValidation.noArgumentsSupported": "命令不支持任何参数", "xpack.securitySolution.console.commandValidation.oneArgIsRequired": "必须至少使用一个参数", - "xpack.securitySolution.console.commandValidation.unknownArgument": "未知参数:{unknownArgs}", "xpack.securitySolution.console.commandValidation.unsupportedArg": "不支持的参数:{argName}", "xpack.securitySolution.console.unknownCommand.title": "未知命令", "xpack.securitySolution.containers.anomalies.errorFetchingAnomaliesData": "无法查询异常数据", From e7816b84562765d5779733a933969da84410e279 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 13 Jun 2022 16:48:19 +0100 Subject: [PATCH 41/62] skip flaky suite (#133916) --- x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts index 81e05b65348ee..0e6e0a499fb3e 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts @@ -142,7 +142,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - describe('tls alert', function () { + // FLAKY: https://github.com/elastic/kibana/issues/g + describe.skip('tls alert', function () { const DEFAULT_DATE_START = 'Sep 10, 2019 @ 12:40:08.078'; const DEFAULT_DATE_END = 'Sep 11, 2019 @ 19:40:08.078'; let alerts: any; From e8ba64e43e2d830acaddbc63a3f83558efa228f1 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 13 Jun 2022 16:49:13 +0100 Subject: [PATCH 42/62] skip flaky suite (#133973) --- test/functional/apps/dashboard/group1/url_field_formatter.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/dashboard/group1/url_field_formatter.ts b/test/functional/apps/dashboard/group1/url_field_formatter.ts index be454549af378..2295693098ba7 100644 --- a/test/functional/apps/dashboard/group1/url_field_formatter.ts +++ b/test/functional/apps/dashboard/group1/url_field_formatter.ts @@ -35,7 +35,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(currentUrl).to.equal(fieldUrl); }; - describe('Changing field formatter to Url', () => { + // FLAKY: https://github.com/elastic/kibana/issues/133973 + describe.skip('Changing field formatter to Url', () => { before(async function () { await kibanaServer.savedObjects.cleanStandardList(); await kibanaServer.importExport.load( From 0d5808a22b105b676cd9545011c112e7211b66c7 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Mon, 13 Jun 2022 09:03:44 -0700 Subject: [PATCH 43/62] [DOCS] Adds Kibana API support to Console (#134111) * [DOCS] Adds Kibana API support to Console * Update docs/dev-tools/console/console.asciidoc Co-authored-by: Kaarina Tungseth Co-authored-by: Kaarina Tungseth --- docs/dev-tools/console/console.asciidoc | 17 ++++++++++++----- docs/user/api.asciidoc | 10 ++++++---- docs/user/dev-tools.asciidoc | 2 +- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/docs/dev-tools/console/console.asciidoc b/docs/dev-tools/console/console.asciidoc index 69f81d838c143..6a28f0f433d46 100644 --- a/docs/dev-tools/console/console.asciidoc +++ b/docs/dev-tools/console/console.asciidoc @@ -1,9 +1,9 @@ [[console-kibana]] -== Run {es} API requests +== Run API requests -Interact with the REST API of {es} with *Console*. You can: +Interact with the REST APIs of {es} and {kib} with *Console*. With *Console*, you can: -* Send requests to {es} and view the responses +* Send requests and view the responses * View API documentation * Get your request history @@ -12,8 +12,6 @@ To get started, open the main menu, click *Dev Tools*, then click *Console*. [role="screenshot"] image::dev-tools/console/images/console.png["Console"] -NOTE: **Console** supports only Elasticsearch APIs. You are unable to interact with the {kib} APIs with **Console** and must use curl or another HTTP tool instead. - [float] [[console-api]] === Write requests @@ -43,6 +41,15 @@ curl -XGET "http://localhost:9200/_search" -d' }' ---------------------------------- +Prepend requests to a {kib} API endpoint with `kbn:` + +[source,bash] +-------------------------------------------------- +`GET kbn:/api/index_management/indices` +-------------------------------------------------- + + + When you paste the command into *Console*, {kib} automatically converts it to *Console* syntax. Alternatively, if you want to see *Console* syntax in cURL, click the action icon (image:dev-tools/console/images/wrench.png[]) and select *Copy as cURL*. diff --git a/docs/user/api.asciidoc b/docs/user/api.asciidoc index 5119762f9ac6b..0cfd4620b7cb5 100644 --- a/docs/user/api.asciidoc +++ b/docs/user/api.asciidoc @@ -9,11 +9,13 @@ deploying {kib}. [[using-apis]] == Using the APIs -Interact with the {kib} APIs through the `curl` command and HTTP and HTTPs protocols. +Prepend any {kib} API endpoint with `kbn:` and send the request through < Console>>. +For example: -It is recommended that you use HTTPs on port 5601 because it is more secure. - -NOTE: The {kib} Console supports only Elasticsearch APIs. You are unable to interact with the {kib} APIs with the Console and must use `curl` or another HTTP tool instead. For more information, refer to <>. +[source,sh] +-------------------------------------------------- +`GET kbn:/api/index_management/indices` +-------------------------------------------------- [float] [[api-authentication]] diff --git a/docs/user/dev-tools.asciidoc b/docs/user/dev-tools.asciidoc index 0c5bef489dd01..48c41ed8886db 100644 --- a/docs/user/dev-tools.asciidoc +++ b/docs/user/dev-tools.asciidoc @@ -12,7 +12,7 @@ with your data. a| <> -| Interact with the REST API of Elasticsearch, including sending requests +| Interact with the REST APIs of {es} and {kib}, including sending requests and viewing API documentation. a| <> From 517395148a1c38df933756fb32a5d50a6ee3a9b9 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 13 Jun 2022 10:10:27 -0600 Subject: [PATCH 44/62] [Security Solution] Fix video to play (#134076) --- .../common/components/landing_cards/index.tsx | 44 ++++++------------- 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/landing_cards/index.tsx b/x-pack/plugins/security_solution/public/common/components/landing_cards/index.tsx index 2349cb4544a54..2ee1898568a2f 100644 --- a/x-pack/plugins/security_solution/public/common/components/landing_cards/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/landing_cards/index.tsx @@ -6,16 +6,7 @@ */ import React, { memo, useMemo } from 'react'; -import { - EuiButton, - EuiCard, - EuiFlexGroup, - EuiFlexItem, - EuiImage, - EuiLink, - EuiPageHeader, - EuiToolTip, -} from '@elastic/eui'; +import { EuiButton, EuiCard, EuiFlexGroup, EuiFlexItem, EuiPageHeader } from '@elastic/eui'; import styled from 'styled-components'; import * as i18n from './translations'; import endpointSvg from '../../images/endpoint1.svg'; @@ -53,13 +44,6 @@ const StyledEuiPageHeader = styled(EuiPageHeader)` } `; -const StyledEuiImage = styled(EuiImage)` - img { - display: block; - margin: 0 auto; - } -`; - const StyledImgEuiCard = styled(EuiCard)` img { margin-top: 20px; @@ -73,8 +57,6 @@ const StyledEuiFlexItem = styled(EuiFlexItem)` margin: -12px !important; `; -const ELASTIC_SECURITY_URL = `elastic.co/security`; - export const LandingCards = memo(() => { const { http: { @@ -82,12 +64,6 @@ export const LandingCards = memo(() => { }, } = useKibana().services; - const tooltipContent = ( - - {ELASTIC_SECURITY_URL} - - ); - const href = useMemo(() => prepend(ADD_DATA_PATH), [prepend]); return ( @@ -108,11 +84,19 @@ export const LandingCards = memo(() => { /> - - - - - +

    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 i9pRhm(YuZ-4*X}<7s_Ym%3U&e diff --git a/docs/management/managing-saved-objects.asciidoc b/docs/management/managing-saved-objects.asciidoc index 39d294df43a5a..ee1247501e8da 100644 --- a/docs/management/managing-saved-objects.asciidoc +++ b/docs/management/managing-saved-objects.asciidoc @@ -97,12 +97,25 @@ limits the number of saved objects which may be exported. === Copy to other {kib} spaces To copy a saved object to another space, click the actions icon image:images/actions_icon.png[Actions icon] -and select *Copy to space*. From here, you can select the spaces in which to copy the object. +and select *Copy to spaces*. From here, you can select the spaces in which to copy the object. You can also select whether to automatically overwrite any conflicts in the target spaces, or resolve them manually. WARNING: The copy operation automatically includes child objects that are related to the saved objects. If you don't want this behavior, use the <> instead. +[float] +[role="xpack"] +[[managing-saved-objects-share-to-space]] +=== Share to other {kib} spaces + +To share a saved object to another space -- which makes a single saved object available in multiple spaces -- click the actions icon +image:images/actions_icon.png[Actions icon] and select *Share to spaces*. From here, you can select the spaces in which to share the object, +or indicate that you want the object to be shared to _all spaces_, which includes those that exist now and any created in the future. + +Not all saved object types are shareable. If an object is shareable, the Spaces column shows which spaces it exists in. You can also click +those space icons to open the Share UI. + +WARNING: The share operation automatically includes child objects that are related to the saved objects. include::saved-objects/saved-object-ids.asciidoc[] From 12796503206dc2a7b932913f9e0295fd201370ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Fri, 10 Jun 2022 00:12:20 +0200 Subject: [PATCH 04/62] Update Backport Github Action v8.5.2 (#134097) --- .github/workflows/backport.yml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 375854b9c54b7..d55634c958289 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -1,10 +1,7 @@ on: pull_request_target: - branches: - - main - types: - - labeled - - closed + branches: ["main"] + types: ["labeled", "closed"] jobs: backport: @@ -19,9 +16,14 @@ jobs: ) steps: - name: Backport Action - uses: sqren/backport-github-action@v7.4.0 + uses: sqren/backport-github-action@v8.5.2 with: github_token: ${{secrets.KIBANAMACHINE_TOKEN}} - - name: Backport log - run: cat ~/.backport/backport.log + - name: Info log + if: ${{ success() }} + run: cat /home/runner/.backport/backport.info.log + + - name: Debug log + if: ${{ failure() }} + run: cat /home/runner/.backport/backport.debug.log From 346840b0321171cd328892741ead5892a2c2f0ad Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Thu, 9 Jun 2022 16:33:11 -0700 Subject: [PATCH 05/62] [Canvas] Fix by-reference embeddable migration error during workpad migration (#133911) * Adds check in embeddable fn migration to migrate only by value embedables * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../canvas_plugin_src/functions/external/embeddable.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts index a74b4c262dd79..204021564d1f0 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts @@ -59,10 +59,12 @@ export function embeddableFunctionFactory({ const embeddableInput = decode(state.arguments.config[0] as string); const embeddableType = state.arguments.type[0]; - const migratedInput = migrateFn({ ...embeddableInput, type: embeddableType }); - state.arguments.config[0] = encode(migratedInput); - state.arguments.type[0] = migratedInput.type as string; + if (embeddableInput.explicitInput.attributes || embeddableInput.explicitInput.savedVis) { + const migratedInput = migrateFn({ ...embeddableInput, type: embeddableType }); + state.arguments.config[0] = encode(migratedInput); + state.arguments.type[0] = migratedInput.type as string; + } return state; }; From 0187f711ee69ad1065858e1db592057183fcfdb3 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Thu, 9 Jun 2022 16:45:22 -0700 Subject: [PATCH 06/62] Revert "[Console] Handle encoded characters in API requests (#132191)" (#134142) This reverts commit 52dca01b231372129a76e885c283a5eaab51e889. --- .../console/server/lib/proxy_request.test.ts | 16 ---------------- .../console/server/lib/proxy_request.ts | 18 ++++++++---------- .../routes/api/console/proxy/create_handler.ts | 1 - 3 files changed, 8 insertions(+), 27 deletions(-) diff --git a/src/plugins/console/server/lib/proxy_request.test.ts b/src/plugins/console/server/lib/proxy_request.test.ts index 8eced59cf2cea..2bb5e481fbb26 100644 --- a/src/plugins/console/server/lib/proxy_request.test.ts +++ b/src/plugins/console/server/lib/proxy_request.test.ts @@ -149,21 +149,5 @@ describe(`Console's send request`, () => { const [httpRequestOptions] = stub.firstCall.args; expect((httpRequestOptions as any).path).toEqual('/%3Cmy-index-%7Bnow%2Fd%7D%3E'); }); - - it('should not encode path if it does not require encoding', async () => { - const result = await proxyRequest({ - agent: null as any, - headers: {}, - method: 'get', - payload: null as any, - timeout: 30000, - uri: new URL(`http://noone.nowhere.none/my-index/_doc/this%2Fis%2Fa%2Fdoc`), - originalPath: 'my-index/_doc/this%2Fis%2Fa%2Fdoc', - }); - - expect(result).toEqual('done'); - const [httpRequestOptions] = stub.firstCall.args; - expect((httpRequestOptions as any).path).toEqual('/my-index/_doc/this%2Fis%2Fa%2Fdoc'); - }); }); }); diff --git a/src/plugins/console/server/lib/proxy_request.ts b/src/plugins/console/server/lib/proxy_request.ts index 62437acbd0ddd..c4fbfd315da4e 100644 --- a/src/plugins/console/server/lib/proxy_request.ts +++ b/src/plugins/console/server/lib/proxy_request.ts @@ -22,7 +22,6 @@ interface Args { timeout: number; headers: http.OutgoingHttpHeaders; rejectUnauthorized?: boolean; - originalPath?: string; } /** @@ -40,6 +39,11 @@ const sanitizeHostname = (hostName: string): string => const encodePathname = (pathname: string) => { const decodedPath = new URLSearchParams(`path=${pathname}`).get('path') ?? ''; + // Skip if it is valid + if (pathname === decodedPath) { + return pathname; + } + return `/${encodeURIComponent(trimStart(decodedPath, '/'))}`; }; @@ -54,17 +58,11 @@ export const proxyRequest = ({ timeout, payload, rejectUnauthorized, - originalPath, }: Args) => { - const { hostname, port, protocol, search, pathname: percentEncodedPath } = uri; + const { hostname, port, protocol, pathname, search } = uri; const client = uri.protocol === 'https:' ? https : http; - let pathname = uri.pathname; + const encodedPath = encodePathname(pathname); let resolved = false; - const requiresEncoding = trimStart(originalPath, '/') !== trimStart(percentEncodedPath, '/'); - - if (requiresEncoding) { - pathname = encodePathname(pathname); - } let resolve: (res: http.IncomingMessage) => void; let reject: (res: unknown) => void; @@ -86,7 +84,7 @@ export const proxyRequest = ({ host: sanitizeHostname(hostname), port: port === '' ? undefined : parseInt(port, 10), protocol, - path: `${pathname}${search || ''}`, + path: `${encodedPath}${search || ''}`, headers: { ...finalUserHeaders, 'content-type': 'application/json', diff --git a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts index 9b9cb0f3b66ef..8cd0400d82cb0 100644 --- a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts +++ b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts @@ -175,7 +175,6 @@ export const createHandler = payload: body, rejectUnauthorized, agent, - originalPath: path, }); break; From 5d3298762068db55ea6842e528ea49b8ac6e76cd Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Thu, 9 Jun 2022 17:58:16 -0600 Subject: [PATCH 07/62] [Security Solution][Detections] Adds confirmation modal with docs link when updating rules if V1/V2 jobs are installed (#128334) ## Summary Resolves https://github.com/elastic/kibana/issues/128121 by adding a confirmation modal with documentation links and list of affected jobs when the user clicks to update rules if any jobs are installed that are being replaced by the new V3 jobs (https://github.com/elastic/detection-rules/pull/1846). There are three touch points for this modal, the `PrePackagedRulesPromptComponent` (empty table UI), the main header buttons on the `RulePageComponent`, and the `UpdatePrePackagedRulesCallOutComponent` (which uses a function wrapped by the `RulePageComponent`). ##### Modal with initial copy: