diff --git a/.bazelrc b/.bazelrc index 158338ec5f093..5fa6ef245fcea 100644 --- a/.bazelrc +++ b/.bazelrc @@ -11,7 +11,7 @@ import %workspace%/.bazelrc.common # BuildBuddy ## Metadata settings -build --workspace_status_command=$(pwd)/src/dev/bazel_workspace_status.sh +build --workspace_status_command="node ./src/dev/bazel_workspace_status.js" # Enable this in case you want to share your build info # build --build_metadata=VISIBILITY=PUBLIC build --build_metadata=TEST_GROUPS=//packages diff --git a/.ci/packer_cache_for_branch.sh b/.ci/packer_cache_for_branch.sh index bbdf5484faf65..ee220537de340 100755 --- a/.ci/packer_cache_for_branch.sh +++ b/.ci/packer_cache_for_branch.sh @@ -26,7 +26,6 @@ source src/dev/ci_setup/setup.sh; # download es snapshots node scripts/es snapshot --download-only; -node scripts/es snapshot --license=oss --download-only; # download reporting browsers (cd "x-pack" && node ../node_modules/.bin/gulp downloadChromium); diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2917cc52a6c6d..34b449346ddf7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -91,6 +91,7 @@ /src/plugins/dashboard/ @elastic/kibana-presentation /src/plugins/input_control_vis/ @elastic/kibana-presentation /src/plugins/vis_type_markdown/ @elastic/kibana-presentation +/test/functional/apps/dashboard/ @elastic/kibana-presentation /x-pack/plugins/canvas/ @elastic/kibana-presentation /x-pack/plugins/dashboard_enhanced/ @elastic/kibana-presentation /x-pack/test/functional/apps/canvas/ @elastic/kibana-presentation @@ -149,6 +150,7 @@ /src/cli/keystore/ @elastic/kibana-operations /src/legacy/server/warnings/ @elastic/kibana-operations /.ci/es-snapshots/ @elastic/kibana-operations +/.github/workflows/ @elastic/kibana-operations /vars/ @elastic/kibana-operations /.bazelignore @elastic/kibana-operations /.bazeliskversion @elastic/kibana-operations @@ -244,7 +246,6 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /x-pack/test/security_api_integration/ @elastic/kibana-security /x-pack/test/security_functional/ @elastic/kibana-security /x-pack/test/spaces_api_integration/ @elastic/kibana-security -#CC# /x-pack/plugins/security_solution/ @elastic/kibana-security #CC# /x-pack/plugins/security/ @elastic/kibana-security # Kibana Alerting Services @@ -312,25 +313,22 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib #CC# /x-pack/plugins/console_extensions/ @elastic/es-ui #CC# /x-pack/plugins/cross_cluster_replication/ @elastic/es-ui -# Endpoint -/x-pack/plugins/endpoint/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/endpoint_api_integration_no_ingest/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/security_solution_endpoint/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/functional/es_archives/endpoint/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/plugin_functional/plugins/resolver_test/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/plugin_functional/test_suites/resolver/ @elastic/endpoint-app-team @elastic/siem -#CC# /x-pack/legacy/plugins/siem/ @elastic/siem -#CC# /x-pack/plugins/siem/ @elastic/siem -#CC# /x-pack/plugins/security_solution/ @elastic/siem - # Security Solution -/x-pack/plugins/security_solution/ @elastic/siem @elastic/endpoint-app-team -/x-pack/test/detection_engine_api_integration @elastic/siem @elastic/endpoint-app-team -/x-pack/test/lists_api_integration @elastic/siem @elastic/endpoint-app-team -/x-pack/test/api_integration/apis/security_solution @elastic/siem @elastic/endpoint-app-team -/x-pack/plugins/case @elastic/siem @elastic/endpoint-app-team -/x-pack/plugins/lists @elastic/siem @elastic/endpoint-app-team -#CC# /x-pack/plugins/security_solution/ @elastic/siem +/x-pack/test/endpoint_api_integration_no_ingest/ @elastic/security-solution +/x-pack/test/security_solution_endpoint/ @elastic/security-solution +/x-pack/test/functional/es_archives/endpoint/ @elastic/security-solution +/x-pack/test/plugin_functional/plugins/resolver_test/ @elastic/security-solution +/x-pack/test/plugin_functional/test_suites/resolver/ @elastic/security-solution +/x-pack/plugins/security_solution/ @elastic/security-solution +/x-pack/test/detection_engine_api_integration @elastic/security-solution +/x-pack/test/lists_api_integration @elastic/security-solution +/x-pack/test/api_integration/apis/security_solution @elastic/security-solution +#CC# /x-pack/plugins/security_solution/ @elastic/security-solution + +# Security Solution sub teams +/x-pack/plugins/case @elastic/security-threat-hunting +/x-pack/test/case_api_integration @elastic/security-threat-hunting +/x-pack/plugins/lists @elastic/security-detections-response # Security Intelligence And Analytics /x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics @@ -362,3 +360,4 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib # Reporting #CC# /x-pack/plugins/reporting/ @elastic/kibana-reporting-services + diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 0000000000000..f64b9e95fbaab --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,46 @@ +on: + pull_request_target: + branches: + - master + types: + - labeled + - closed + +jobs: + backport: + name: Backport PR + if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'auto-backport') + runs-on: ubuntu-latest + + steps: + - name: 'Get backport config' + run: | + curl 'https://raw.githubusercontent.com/elastic/kibana/master/.backportrc.json' > .backportrc.json + + - name: Use Node.js 14.x + uses: actions/setup-node@v1 + with: + node-version: 14.x + + - name: Install backport CLI + run: npm install -g backport@5.6.4 + + - name: Backport PR + run: | + git config --global user.name "kibanamachine" + git config --global user.email "42973632+kibanamachine@users.noreply.github.com" + backport --fork true --username kibanamachine --accessToken "${{ secrets.KIBANAMACHINE_TOKEN }}" --ci --pr "$PR_NUMBER" --labels backport --assignee "$PR_OWNER" | tee 'output.log' + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_OWNER: ${{ github.event.pull_request.user.login }} + + - name: Report backport status + run: | + COMMENT="Backport result + \`\`\` + $(cat output.log) + \`\`\`" + + GITHUB_TOKEN="${{ secrets.KIBANAMACHINE_TOKEN }}" gh api -X POST repos/elastic/kibana/issues/$PR_NUMBER/comments -F body="$COMMENT" + env: + PR_NUMBER: ${{ github.event.pull_request.number }} diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md index 5c1a6a0393c2e..034f9c70e389f 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md @@ -4,11 +4,20 @@ ## EmbeddableStateTransfer.clearEditorState() method +Clears the [editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) from the sessionStorage for the provided app id + Signature: ```typescript -clearEditorState(): void; +clearEditorState(appId: string): void; ``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| appId | string | The app to fetch incomingEditorState for | + Returns: `void` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md index 1434de2c9870e..cd261bff5905b 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md @@ -4,18 +4,19 @@ ## EmbeddableStateTransfer.getIncomingEditorState() method -Fetches an [originating app](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) argument from the sessionStorage +Fetches an [editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) from the sessionStorage for the provided app id Signature: ```typescript -getIncomingEditorState(removeAfterFetch?: boolean): EmbeddableEditorState | undefined; +getIncomingEditorState(appId: string, removeAfterFetch?: boolean): EmbeddableEditorState | undefined; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | +| appId | string | The app to fetch incomingEditorState for | | removeAfterFetch | boolean | Whether to remove the package state after fetch to prevent duplicates. | Returns: diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md index 9ead71f0bb22c..47873c8e91e41 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md @@ -4,18 +4,19 @@ ## EmbeddableStateTransfer.getIncomingEmbeddablePackage() method -Fetches an [embeddable package](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) argument from the sessionStorage +Fetches an [embeddable package](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) from the sessionStorage for the given AppId Signature: ```typescript -getIncomingEmbeddablePackage(removeAfterFetch?: boolean): EmbeddablePackageState | undefined; +getIncomingEmbeddablePackage(appId: string, removeAfterFetch?: boolean): EmbeddablePackageState | undefined; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | +| appId | string | The app to fetch EmbeddablePackageState for | | removeAfterFetch | boolean | Whether to remove the package state after fetch to prevent duplicates. | Returns: diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.md index 76b6708b93bd1..13c6c8c0325f1 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.md @@ -29,9 +29,9 @@ export declare class EmbeddableStateTransfer | Method | Modifiers | Description | | --- | --- | --- | -| [clearEditorState()](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md) | | | -| [getIncomingEditorState(removeAfterFetch)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md) | | Fetches an [originating app](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) argument from the sessionStorage | -| [getIncomingEmbeddablePackage(removeAfterFetch)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md) | | Fetches an [embeddable package](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) argument from the sessionStorage | +| [clearEditorState(appId)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md) | | Clears the [editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) from the sessionStorage for the provided app id | +| [getIncomingEditorState(appId, removeAfterFetch)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md) | | Fetches an [editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) from the sessionStorage for the provided app id | +| [getIncomingEmbeddablePackage(appId, removeAfterFetch)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md) | | Fetches an [embeddable package](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) from the sessionStorage for the given AppId | | [navigateToEditor(appId, options)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.navigatetoeditor.md) | | A wrapper around the method which navigates to the specified appId with [embeddable editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) | | [navigateToWithEmbeddablePackage(appId, options)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.navigatetowithembeddablepackage.md) | | A wrapper around the method which navigates to the specified appId with [embeddable package state](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) | diff --git a/docs/user/dashboard/dashboard.asciidoc b/docs/user/dashboard/dashboard.asciidoc index 8b3eddc008500..3c86c37f1fd30 100644 --- a/docs/user/dashboard/dashboard.asciidoc +++ b/docs/user/dashboard/dashboard.asciidoc @@ -133,15 +133,17 @@ image::dashboard/images/dashboard-filters.png[Labeled interface with semi-struct Semi-structured search:: Combine free text search with field-based search using the <>. Type a search term to match across all fields, or begin typing a field name to - get prompted with field names and operators you can use to build a structured query. - + + get prompted with field names and operators you can use to build a structured query. For example, in the sample web logs data, this query displays data only for the US: - . Enter `g`, and then select *geo.source*. - . Select *equals some value* and *US*, and then click *Update*. + . Enter `g`, then select *geo.source*. + . Select *equals some value* and *US*, then click *Update*. . For a more complex search, try: - `geo.src : "US" and url.keyword : "https://www.elastic.co/downloads/beats/metricbeat"` +[source,text] +------------------- +geo.src : "US" and url.keyword : "https://www.elastic.co/downloads/beats/metricbeat" +------------------- Time filter:: Dashboards have a global time filter that restricts the data that displays, but individual panels can @@ -152,21 +154,18 @@ Time filter:: . Open the panel menu, then select *More > Customize time range*. . On the *Customize panel time range* window, specify the new time range, then click *Add to panel*. - + [role="screenshot"] image:images/time_range_per_panel.gif[Time range per dashboard panel] Additional filters with AND:: - You can add filters to a dashboard, or pin filters to multiple places in {kib}. To add filters, using a basic editor or an advanced JSON editor for the {es} {ref}/query-dsl.html[query DSL]. - + Add filters to a dashboard, or pin filters to multiple places in {kib}. To add filters, using a basic editor or an advanced JSON editor for the {es} {ref}/query-dsl.html[query DSL]. When you use more than one index pattern on a dashboard, the filter editor allows you to filter only one dashboard. - To dynamically add filters, click a series on a dashboard. For example, to filter the dashboard to display only ios data: - . Click *Add filter*. . Set *Field* to *machine.os*, *Operator* to *is*, and *Value* to *ios*. . *Save* the filter. - . To remove the filter, click *x* next to the filter. + . To remove the filter, click *x*. [float] [[clone-panels]] diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts index cb5175142c160..93826cf3add80 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts @@ -109,7 +109,7 @@ export class CiStatsReporter { }, }); - return; + return true; } catch (error) { if (!error?.request) { // not an axios error, must be a usage error that we should notify user about diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts index 244af7b657418..1ee78518bb801 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts @@ -10,7 +10,7 @@ import Path from 'path'; import Fs from 'fs'; import { CiStatsReporter } from './ci_stats_reporter'; -import { run, createFlagError } from '../run'; +import { run, createFlagError, createFailError } from '../run'; export function shipCiStatsCli() { run( @@ -23,12 +23,20 @@ export function shipCiStatsCli() { } const reporter = CiStatsReporter.fromEnv(log); + + if (!reporter.isEnabled()) { + throw createFailError('unable to initilize the CI Stats reporter'); + } + for (const path of metricPaths) { // resolve path from CLI relative to CWD const abs = Path.resolve(path); const json = Fs.readFileSync(abs, 'utf8'); - await reporter.metrics(JSON.parse(json)); - log.success('shipped metrics from', path); + if (await reporter.metrics(JSON.parse(json))) { + log.success('shipped metrics from', path); + } else { + throw createFailError('failed to ship metrics'); + } } }, { diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 46471a4e9dac7..4fd28678d2653 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -169,7 +169,7 @@ export const schema = Joi.object() esTestCluster: Joi.object() .keys({ - license: Joi.string().default('oss'), + license: Joi.string().default('basic'), from: Joi.string().default('snapshot'), serverArgs: Joi.array(), serverEnvVars: Joi.object(), diff --git a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js b/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js index c04564279a971..43b6c90452b81 100644 --- a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js +++ b/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js @@ -22,7 +22,7 @@ export function createLegacyEsTestCluster(options = {}) { const { port = esTestConfig.getPort(), password = 'changeme', - license = 'oss', + license = 'basic', log, basePath = resolve(KIBANA_ROOT, '.es'), esFrom = esTestConfig.getBuildFrom(), diff --git a/renovate.json5 b/renovate.json5 index 1585627daa880..f1e773427a103 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -57,7 +57,7 @@ groupName: 'vega related modules', packageNames: ['vega', 'vega-lite', 'vega-schema-url-parser', 'vega-tooltip'], reviewers: ['team:kibana-app'], - labels: ['Feature:Lens', 'Team:KibanaApp'], + labels: ['Feature:Vega', 'Team:KibanaApp'], enabled: true, }, ], diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts index 2d3ab91697e42..317bfe33b3a19 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts @@ -30,7 +30,7 @@ describe('migration v2', () => { adjustTimeout: (t: number) => jest.setTimeout(t), settings: { es: { - license: oss ? 'oss' : 'trial', + license: 'trial', dataArchive, }, }, diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts index bce01c93fe886..16ba0c855867c 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts @@ -32,7 +32,7 @@ describe.skip('migration from 7.7.2-xpack with 100k objects', () => { adjustTimeout: (t: number) => jest.setTimeout(600000), settings: { es: { - license: oss ? 'oss' : 'trial', + license: 'trial', dataArchive, }, }, diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts index 011ba67a05512..14f614643ac9f 100644 --- a/src/core/test_helpers/kbn_server.ts +++ b/src/core/test_helpers/kbn_server.ts @@ -185,7 +185,7 @@ export function createTestServers({ adjustTimeout: (timeout: number) => void; settings?: { es?: { - license: 'oss' | 'basic' | 'gold' | 'trial'; + license: 'basic' | 'gold' | 'trial'; [key: string]: any; }; kbn?: { @@ -208,7 +208,7 @@ export function createTestServers({ if (!adjustTimeout) { throw new Error('adjustTimeout is required in order to avoid flaky tests'); } - const license = get(settings, 'es.license', 'oss'); + const license = get(settings, 'es.license', 'basic'); const usersToBeAdded = get(settings, 'users', []); if (usersToBeAdded.length > 0) { if (license !== 'trial') { diff --git a/src/dev/bazel_workspace_status.js b/src/dev/bazel_workspace_status.js new file mode 100644 index 0000000000000..fe60f9176d243 --- /dev/null +++ b/src/dev/bazel_workspace_status.js @@ -0,0 +1,80 @@ +/* + * 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. + */ + +// Inspired on https://github.com/buildbuddy-io/buildbuddy/blob/master/workspace_status.sh +// This script will be run bazel when building process starts to +// generate key-value information that represents the status of the +// workspace. The output should be like +// +// KEY1 VALUE1 +// KEY2 VALUE2 +// +// If the script exits with non-zero code, it's considered as a failure +// and the output will be discarded. + +(async () => { + const execa = require('execa'); + const os = require('os'); + + async function runCmd(cmd, args) { + try { + return await execa(cmd, args); + } catch (e) { + return { exitCode: 1 }; + } + } + + // Git repo + const kbnGitOriginName = process.env.KBN_GIT_ORIGIN_NAME || 'origin'; + const repoUrlCmdResult = await runCmd('git', [ + 'config', + '--get', + `remote.${kbnGitOriginName}.url`, + ]); + if (repoUrlCmdResult.exitCode === 0) { + // Only output REPO_URL when found it + console.log(`REPO_URL ${repoUrlCmdResult.stdout}`); + } + + // Commit SHA + const commitSHACmdResult = await runCmd('git', ['rev-parse', 'HEAD']); + if (commitSHACmdResult.exitCode !== 0) { + process.exit(1); + } + console.log(`COMMIT_SHA ${commitSHACmdResult.stdout}`); + + // Git branch + const gitBranchCmdResult = await runCmd('git', ['rev-parse', '--abbrev-ref', 'HEAD']); + if (gitBranchCmdResult.exitCode !== 0) { + process.exit(1); + } + console.log(`GIT_BRANCH ${gitBranchCmdResult.stdout}`); + + // Tree status + const treeStatusCmdResult = await runCmd('git', ['diff-index', '--quiet', 'HEAD', '--']); + const treeStatusVarStr = 'GIT_TREE_STATUS'; + if (treeStatusCmdResult.exitCode === 0) { + console.log(`${treeStatusVarStr} Clean`); + } else { + console.log(`${treeStatusVarStr} Modified`); + } + + // Host + if (process.env.CI) { + const hostCmdResult = await runCmd('hostname'); + const hostStr = hostCmdResult.stdout.split('-').slice(0, -1).join('-'); + const coresStr = os.cpus().filter((cpu, index) => { + return !cpu.model.includes('Intel') || index % 2 === 1; + }).length; + + if (hostCmdResult.exitCode !== 0) { + process.exit(1); + } + console.log(`HOST ${hostStr}-${coresStr}`); + } +})(); diff --git a/src/dev/bazel_workspace_status.sh b/src/dev/bazel_workspace_status.sh deleted file mode 100755 index efaca4bb98849..0000000000000 --- a/src/dev/bazel_workspace_status.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash - -# Inspired on https://github.com/buildbuddy-io/buildbuddy/blob/master/workspace_status.sh -# This script will be run bazel when building process starts to -# generate key-value information that represents the status of the -# workspace. The output should be like -# -# KEY1 VALUE1 -# KEY2 VALUE2 -# -# If the script exits with non-zero code, it's considered as a failure -# and the output will be discarded. - -# Git repo -repo_url=$(git config --get remote.origin.url) -if [[ $? != 0 ]]; -then - exit 1 -fi -echo "REPO_URL ${repo_url}" - -# Commit SHA -commit_sha=$(git rev-parse HEAD) -if [[ $? != 0 ]]; -then - exit 1 -fi -echo "COMMIT_SHA ${commit_sha}" - -# Git branch -repo_url=$(git rev-parse --abbrev-ref HEAD) -if [[ $? != 0 ]]; -then - exit 1 -fi -echo "GIT_BRANCH ${repo_url}" - -# Tree status -git diff-index --quiet HEAD -- -if [[ $? == 0 ]]; -then - tree_status="Clean" -else - tree_status="Modified" -fi -echo "GIT_TREE_STATUS ${tree_status}" - -# Host -if [ "$CI" = "true" ]; then - host=$(hostname | sed 's|\(.*\)-.*|\1|') - cores=$(grep ^cpu\\scores /proc/cpuinfo | uniq | awk '{print $4}' ) - if [[ $? != 0 ]]; - then - exit 1 - fi - echo "HOST ${host}-${cores}" -fi diff --git a/src/dev/ci_setup/setup.sh b/src/dev/ci_setup/setup.sh index 0b24f0b22b81a..db7110d2d0875 100755 --- a/src/dev/ci_setup/setup.sh +++ b/src/dev/ci_setup/setup.sh @@ -32,8 +32,6 @@ yarn kbn bootstrap ### echo " -- downloading es snapshot" node scripts/es snapshot --download-only; -node scripts/es snapshot --license=oss --download-only; - ### ### verify no git modifications diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 0b835d4b9fa94..2deafaaf35a94 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -180,6 +180,15 @@ fi ### cp -f "$KIBANA_DIR/src/dev/ci_setup/.bazelrc-ci" "$HOME/.bazelrc"; +### +### remove write permissions on buildbuddy remote cache for prs +### +if [[ "$ghprbPullId" ]] ; then + echo "# Appended by $KIBANA_DIR/src/dev/ci_setup/setup.sh" >> "$HOME/.bazelrc" + echo "# Uploads logs & artifacts without writing to cache" >> "$HOME/.bazelrc" + echo "build --noremote_upload_local_results" >> "$HOME/.bazelrc" +fi + ### ### append auth token to buildbuddy into "$HOME/.bazelrc"; ### diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx index 5d384ed8ebd82..ef730e16bc5cf 100644 --- a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx +++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx @@ -22,7 +22,7 @@ import { NotificationsStart } from '../../services/core'; import { dashboardAddToLibraryAction } from '../../dashboard_strings'; import { DashboardPanelState, DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '..'; -export const ACTION_ADD_TO_LIBRARY = 'addToFromLibrary'; +export const ACTION_ADD_TO_LIBRARY = 'saveToLibrary'; export interface AddToLibraryActionContext { embeddable: IEmbeddable; diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts index b27322b6bec53..d12fea07bdd41 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts @@ -21,7 +21,7 @@ import { import { DashboardStateManager } from '../dashboard_state_manager'; import { getDashboardContainerInput, getSearchSessionIdFromURL } from '../dashboard_app_functions'; -import { DashboardContainer, DashboardContainerInput } from '../..'; +import { DashboardConstants, DashboardContainer, DashboardContainerInput } from '../..'; import { DashboardAppServices } from '../types'; import { DASHBOARD_CONTAINER_TYPE } from '..'; @@ -68,7 +68,9 @@ export const useDashboardContainer = ( searchSession.restore(searchSessionIdFromURL); } - const incomingEmbeddable = embeddable.getStateTransfer().getIncomingEmbeddablePackage(true); + const incomingEmbeddable = embeddable + .getStateTransfer() + .getIncomingEmbeddablePackage(DashboardConstants.DASHBOARDS_ID, true); let canceled = false; let pendingContainer: DashboardContainer | ErrorEmbeddable | null | undefined; diff --git a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts index 763186fc17c0c..a8ecb384f782b 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts @@ -42,6 +42,10 @@ describe('embeddable state transfer', () => { const destinationApp = 'superUltraVisualize'; const originatingApp = 'superUltraTestDashboard'; + const testAppId = 'testApp'; + + const buildKey = (appId: string, key: string) => `${appId}-${key}`; + beforeEach(() => { currentAppId$ = new Subject(); currentAppId$.next(originatingApp); @@ -82,7 +86,9 @@ describe('embeddable state transfer', () => { it('can send an outgoing editor state', async () => { await stateTransfer.navigateToEditor(destinationApp, { state: { originatingApp } }); expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_EDITOR_STATE_KEY]: { originatingApp: 'superUltraTestDashboard' }, + [buildKey(destinationApp, EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'superUltraTestDashboard', + }, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { path: undefined, @@ -98,7 +104,9 @@ describe('embeddable state transfer', () => { }); expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { kibanaIsNowForSports: 'extremeSportsKibana', - [EMBEDDABLE_EDITOR_STATE_KEY]: { originatingApp: 'superUltraTestDashboard' }, + [buildKey(destinationApp, EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'superUltraTestDashboard', + }, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { path: undefined, @@ -117,7 +125,10 @@ describe('embeddable state transfer', () => { state: { type: 'coolestType', input: { savedObjectId: '150' } }, }); expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_PACKAGE_STATE_KEY]: { type: 'coolestType', input: { savedObjectId: '150' } }, + [buildKey(destinationApp, EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'coolestType', + input: { savedObjectId: '150' }, + }, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { path: undefined, @@ -133,7 +144,10 @@ describe('embeddable state transfer', () => { }); expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { kibanaIsNowForSports: 'extremeSportsKibana', - [EMBEDDABLE_PACKAGE_STATE_KEY]: { type: 'coolestType', input: { savedObjectId: '150' } }, + [buildKey(destinationApp, EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'coolestType', + input: { savedObjectId: '150' }, + }, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { path: undefined, @@ -151,42 +165,92 @@ describe('embeddable state transfer', () => { it('can fetch an incoming editor state', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_EDITOR_STATE_KEY]: { originatingApp: 'superUltraTestDashboard' }, + [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'superUltraTestDashboard', + }, + }); + const fetchedState = stateTransfer.getIncomingEditorState(testAppId); + expect(fetchedState).toEqual({ originatingApp: 'superUltraTestDashboard' }); + }); + + it('can fetch an incoming editor state and ignore state for other apps', async () => { + store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { + [buildKey('otherApp1', EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'whoops not me', + }, + [buildKey('otherApp2', EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'otherTestDashboard', + }, + [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'superUltraTestDashboard', + }, }); - const fetchedState = stateTransfer.getIncomingEditorState(); + const fetchedState = stateTransfer.getIncomingEditorState(testAppId); expect(fetchedState).toEqual({ originatingApp: 'superUltraTestDashboard' }); + + const fetchedState2 = stateTransfer.getIncomingEditorState('otherApp2'); + expect(fetchedState2).toEqual({ originatingApp: 'otherTestDashboard' }); }); it('incoming editor state returns undefined when state is not in the right shape', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_EDITOR_STATE_KEY]: { helloSportsKibana: 'superUltraTestDashboard' }, + [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: { + helloSportsKibana: 'superUltraTestDashboard', + }, }); - const fetchedState = stateTransfer.getIncomingEditorState(); + const fetchedState = stateTransfer.getIncomingEditorState(testAppId); expect(fetchedState).toBeUndefined(); }); it('can fetch an incoming embeddable package state', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_PACKAGE_STATE_KEY]: { type: 'skisEmbeddable', input: { savedObjectId: '123' } }, + [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'skisEmbeddable', + input: { savedObjectId: '123' }, + }, }); - const fetchedState = stateTransfer.getIncomingEmbeddablePackage(); + const fetchedState = stateTransfer.getIncomingEmbeddablePackage(testAppId); expect(fetchedState).toEqual({ type: 'skisEmbeddable', input: { savedObjectId: '123' } }); }); + it('can fetch an incoming embeddable package state and ignore state for other apps', async () => { + store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { + [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'skisEmbeddable', + input: { savedObjectId: '123' }, + }, + [buildKey('testApp2', EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'crossCountryEmbeddable', + input: { savedObjectId: '456' }, + }, + }); + const fetchedState = stateTransfer.getIncomingEmbeddablePackage(testAppId); + expect(fetchedState).toEqual({ type: 'skisEmbeddable', input: { savedObjectId: '123' } }); + + const fetchedState2 = stateTransfer.getIncomingEmbeddablePackage('testApp2'); + expect(fetchedState2).toEqual({ + type: 'crossCountryEmbeddable', + input: { savedObjectId: '456' }, + }); + }); + it('embeddable package state returns undefined when state is not in the right shape', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_PACKAGE_STATE_KEY]: { kibanaIsFor: 'sports' }, + [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: { kibanaIsFor: 'sports' }, }); - const fetchedState = stateTransfer.getIncomingEmbeddablePackage(); + const fetchedState = stateTransfer.getIncomingEmbeddablePackage(testAppId); expect(fetchedState).toBeUndefined(); }); it('removes embeddable package key when removeAfterFetch is true', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_PACKAGE_STATE_KEY]: { type: 'coolestType', input: { savedObjectId: '150' } }, + [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'coolestType', + input: { savedObjectId: '150' }, + }, iSHouldStillbeHere: 'doing the sports thing', }); - stateTransfer.getIncomingEmbeddablePackage(true); + stateTransfer.getIncomingEmbeddablePackage(testAppId, true); expect(store.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)).toEqual({ iSHouldStillbeHere: 'doing the sports thing', }); @@ -194,10 +258,12 @@ describe('embeddable state transfer', () => { it('removes editor state key when removeAfterFetch is true', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_EDITOR_STATE_KEY]: { originatingApp: 'superCoolFootballDashboard' }, + [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'superCoolFootballDashboard', + }, iSHouldStillbeHere: 'doing the sports thing', }); - stateTransfer.getIncomingEditorState(true); + stateTransfer.getIncomingEditorState(testAppId, true); expect(store.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)).toEqual({ iSHouldStillbeHere: 'doing the sports thing', }); diff --git a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts index d3b1c1c76aadf..8664a5aae7345 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts @@ -50,13 +50,18 @@ export class EmbeddableStateTransfer { public getAppNameFromId = (appId: string): string | undefined => this.appList?.get(appId)?.title; /** - * Fetches an {@link EmbeddableEditorState | originating app} argument from the sessionStorage + * Fetches an {@link EmbeddableEditorState | editor state} from the sessionStorage for the provided app id * + * @param appId - The app to fetch incomingEditorState for * @param removeAfterFetch - Whether to remove the package state after fetch to prevent duplicates. */ - public getIncomingEditorState(removeAfterFetch?: boolean): EmbeddableEditorState | undefined { + public getIncomingEditorState( + appId: string, + removeAfterFetch?: boolean + ): EmbeddableEditorState | undefined { return this.getIncomingState( isEmbeddableEditorState, + appId, EMBEDDABLE_EDITOR_STATE_KEY, { keysToRemoveAfterFetch: removeAfterFetch ? [EMBEDDABLE_EDITOR_STATE_KEY] : undefined, @@ -64,24 +69,33 @@ export class EmbeddableStateTransfer { ); } - public clearEditorState() { + /** + * Clears the {@link EmbeddableEditorState | editor state} from the sessionStorage for the provided app id + * + * @param appId - The app to fetch incomingEditorState for + * @param removeAfterFetch - Whether to remove the package state after fetch to prevent duplicates. + */ + public clearEditorState(appId: string) { const currentState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY); if (currentState) { - delete currentState[EMBEDDABLE_EDITOR_STATE_KEY]; + delete currentState[this.buildKey(appId, EMBEDDABLE_EDITOR_STATE_KEY)]; this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, currentState); } } /** - * Fetches an {@link EmbeddablePackageState | embeddable package} argument from the sessionStorage + * Fetches an {@link EmbeddablePackageState | embeddable package} from the sessionStorage for the given AppId * + * @param appId - The app to fetch EmbeddablePackageState for * @param removeAfterFetch - Whether to remove the package state after fetch to prevent duplicates. */ public getIncomingEmbeddablePackage( + appId: string, removeAfterFetch?: boolean ): EmbeddablePackageState | undefined { return this.getIncomingState( isEmbeddablePackageState, + appId, EMBEDDABLE_PACKAGE_STATE_KEY, { keysToRemoveAfterFetch: removeAfterFetch ? [EMBEDDABLE_PACKAGE_STATE_KEY] : undefined, @@ -122,20 +136,27 @@ export class EmbeddableStateTransfer { }); } + private buildKey(appId: string, key: string) { + return `${appId}-${key}`; + } + private getIncomingState( guard: (state: unknown) => state is IncomingStateType, + appId: string, key: string, options?: { keysToRemoveAfterFetch?: string[]; } ): IncomingStateType | undefined { - const incomingState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)?.[key]; + const incomingState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)?.[ + this.buildKey(appId, key) + ]; const castState = !guard || guard(incomingState) ? (cloneDeep(incomingState) as IncomingStateType) : undefined; if (castState && options?.keysToRemoveAfterFetch) { const stateReplace = { ...this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY) }; options.keysToRemoveAfterFetch.forEach((keyToRemove: string) => { - delete stateReplace[keyToRemove]; + delete stateReplace[this.buildKey(appId, keyToRemove)]; }); this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, stateReplace); } @@ -150,9 +171,9 @@ export class EmbeddableStateTransfer { const stateObject = options?.appendToExistingState ? { ...this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY), - [key]: options.state, + [this.buildKey(appId, key)]: options.state, } - : { [key]: options?.state }; + : { [this.buildKey(appId, key)]: options?.state }; this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, stateObject); await this.navigateToApp(appId, { path: options?.path }); } diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 2f9b43121b45a..3e7014d54958d 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -590,11 +590,10 @@ export class EmbeddableStateTransfer { // Warning: (ae-forgotten-export) The symbol "ApplicationStart" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "PublicAppInfo" needs to be exported by the entry point index.d.ts constructor(navigateToApp: ApplicationStart['navigateToApp'], currentAppId$: ApplicationStart['currentAppId$'], appList?: ReadonlyMap | undefined, customStorage?: Storage); - // (undocumented) - clearEditorState(): void; + clearEditorState(appId: string): void; getAppNameFromId: (appId: string) => string | undefined; - getIncomingEditorState(removeAfterFetch?: boolean): EmbeddableEditorState | undefined; - getIncomingEmbeddablePackage(removeAfterFetch?: boolean): EmbeddablePackageState | undefined; + getIncomingEditorState(appId: string, removeAfterFetch?: boolean): EmbeddableEditorState | undefined; + getIncomingEmbeddablePackage(appId: string, removeAfterFetch?: boolean): EmbeddablePackageState | undefined; // (undocumented) isTransferInProgress: boolean; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ApplicationStart" diff --git a/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx b/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx index 6ca6efaa89797..fa0e0bd5f48f0 100644 --- a/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx +++ b/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx @@ -34,7 +34,7 @@ export const VisualizeByValueEditor = ({ onAppLeave }: VisualizeAppProps) => { useEffect(() => { const { originatingApp: value, embeddableId: embeddableIdValue, valueInput: valueInputValue } = - services.stateTransferService.getIncomingEditorState() || {}; + services.stateTransferService.getIncomingEditorState(VisualizeConstants.APP_ID) || {}; setOriginatingApp(value); setValueInput(valueInputValue); setEmbeddableId(embeddableIdValue); diff --git a/src/plugins/visualize/public/application/components/visualize_editor.tsx b/src/plugins/visualize/public/application/components/visualize_editor.tsx index 7465e7eaa9044..c6333e978183f 100644 --- a/src/plugins/visualize/public/application/components/visualize_editor.tsx +++ b/src/plugins/visualize/public/application/components/visualize_editor.tsx @@ -22,6 +22,7 @@ import { import { VisualizeServices } from '../types'; import { VisualizeEditorCommon } from './visualize_editor_common'; import { VisualizeAppProps } from '../app'; +import { VisualizeConstants } from '../..'; export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { const { id: visualizationIdFromUrl } = useParams<{ id: string }>(); @@ -54,7 +55,8 @@ export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { useLinkedSearchUpdates(services, eventEmitter, appState, savedVisInstance); useEffect(() => { - const { originatingApp: value } = services.stateTransferService.getIncomingEditorState() || {}; + const { originatingApp: value } = + services.stateTransferService.getIncomingEditorState(VisualizeConstants.APP_ID) || {}; setOriginatingApp(value); }, [services]); diff --git a/src/plugins/visualize/public/application/components/visualize_listing.tsx b/src/plugins/visualize/public/application/components/visualize_listing.tsx index c772554344cb2..bc766d63db5a7 100644 --- a/src/plugins/visualize/public/application/components/visualize_listing.tsx +++ b/src/plugins/visualize/public/application/components/visualize_listing.tsx @@ -65,7 +65,7 @@ export const VisualizeListing = () => { useMount(() => { // Reset editor state if the visualize listing page is loaded. - stateTransferService.clearEditorState(); + stateTransferService.clearEditorState(VisualizeConstants.APP_ID); chrome.setBreadcrumbs([ { text: i18n.translate('visualize.visualizeListingBreadcrumbsTitle', { diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index 9ea42e8b56559..e8c3289d4ce41 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -142,7 +142,7 @@ export const getTopNavConfig = ( if (setOriginatingApp && originatingApp && newlyCreated) { setOriginatingApp(undefined); // remove editor state so the connection is still broken after reload - stateTransfer.clearEditorState(); + stateTransfer.clearEditorState(VisualizeConstants.APP_ID); } chrome.docTitle.change(savedVis.lastSavedTitle); chrome.setBreadcrumbs(getEditBreadcrumbs({}, savedVis.lastSavedTitle)); diff --git a/src/plugins/visualize/public/application/visualize_constants.ts b/src/plugins/visualize/public/application/visualize_constants.ts index 7dbf5be77b74d..6e901882a9365 100644 --- a/src/plugins/visualize/public/application/visualize_constants.ts +++ b/src/plugins/visualize/public/application/visualize_constants.ts @@ -16,4 +16,5 @@ export const VisualizeConstants = { CREATE_PATH: '/create', EDIT_PATH: '/edit', EDIT_BY_VALUE_PATH: '/edit_by_value', + APP_ID: 'visualize', }; diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 3d82e6c60a1b6..4eb2d6fd2a731 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -132,7 +132,7 @@ export class VisualizePlugin setUISettings(core.uiSettings); core.application.register({ - id: 'visualize', + id: VisualizeConstants.APP_ID, title: 'Visualize', order: 8000, euiIconType: 'logoKibana', @@ -147,7 +147,9 @@ export class VisualizePlugin // allows the urlTracker to only save URLs that are not linked to an originatingApp this.isLinkedToOriginatingApp = () => { return Boolean( - pluginsStart.embeddable.getStateTransfer().getIncomingEditorState()?.originatingApp + pluginsStart.embeddable + .getStateTransfer() + .getIncomingEditorState(VisualizeConstants.APP_ID)?.originatingApp ); }; diff --git a/test/api_integration/config.js b/test/api_integration/config.js index d688c31dc47e7..bd8f10606a45a 100644 --- a/test/api_integration/config.js +++ b/test/api_integration/config.js @@ -19,7 +19,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'API Integration Tests', }, - esTestCluster: commonConfig.get('esTestCluster'), + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...functionalConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/common/config.js b/test/common/config.js index 9d108f05fd1fc..46cd07b2ec370 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -21,9 +21,7 @@ export default function () { servers, esTestCluster: { - license: 'oss', - from: 'snapshot', - serverArgs: [], + serverArgs: ['xpack.security.enabled=false'], }, kbnTestServer: { diff --git a/test/common/services/deployment.ts b/test/common/services/deployment.ts index a19118bb3065a..510124ce3d1b7 100644 --- a/test/common/services/deployment.ts +++ b/test/common/services/deployment.ts @@ -35,17 +35,7 @@ export function DeploymentProvider({ getService }: FtrProviderContext) { * Useful for functional testing in cloud environment */ async isOss() { - const baseUrl = this.getEsHostPort(); - const username = config.get('servers.elasticsearch.username'); - const password = config.get('servers.elasticsearch.password'); - const response = await fetch(baseUrl + '/_xpack', { - method: 'get', - headers: { - 'Content-Type': 'application/json', - Authorization: 'Basic ' + Buffer.from(username + ':' + password).toString('base64'), - }, - }); - return response.status !== 200; + return config.get('kbnTestServer.serverArgs').indexOf('--oss') > -1; }, async isCloud(): Promise { diff --git a/test/examples/config.js b/test/examples/config.js index fd1ad671cf4bf..0ba7af0bfceb7 100644 --- a/test/examples/config.js +++ b/test/examples/config.js @@ -34,7 +34,10 @@ export default async function ({ readConfigFile }) { }, pageObjects: functionalConfig.get('pageObjects'), servers: functionalConfig.get('servers'), - esTestCluster: functionalConfig.get('esTestCluster'), + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, apps: functionalConfig.get('apps'), esArchiver: { directory: path.resolve(__dirname, '../es_archives'), diff --git a/test/functional/apps/dashboard/embeddable_library.ts b/test/functional/apps/dashboard/embeddable_library.ts new file mode 100644 index 0000000000000..20fe9aeb1387a --- /dev/null +++ b/test/functional/apps/dashboard/embeddable_library.ts @@ -0,0 +1,111 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'settings', 'common']); + const esArchiver = getService('esArchiver'); + const find = getService('find'); + const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); + const dashboardAddPanel = getService('dashboardAddPanel'); + const panelActions = getService('dashboardPanelActions'); + + describe('embeddable library', () => { + before(async () => { + await esArchiver.load('dashboard/current/kibana'); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.clickNewDashboard(); + }); + + it('unlink visualize panel from embeddable library', async () => { + // add heatmap panel from library + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('Rendering Test: heatmap'); + await find.clickByButtonText('Rendering Test: heatmap'); + await dashboardAddPanel.closeAddPanel(); + + const originalPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:heatmap'); + await panelActions.unlinkFromLibary(originalPanel); + await testSubjects.existOrFail('unlinkPanelSuccess'); + + const updatedPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:heatmap'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(false); + + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('Rendering Test: heatmap'); + await find.existsByLinkText('Rendering Test: heatmap'); + await dashboardAddPanel.closeAddPanel(); + }); + + it('save visualize panel to embeddable library', async () => { + const originalPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:heatmap'); + await panelActions.saveToLibrary('Rendering Test: heatmap - copy', originalPanel); + await testSubjects.existOrFail('addPanelToLibrarySuccess'); + + const updatedPanel = await testSubjects.find( + 'embeddablePanelHeading-RenderingTest:heatmap-copy' + ); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(true); + }); + + it('unlink map panel from embeddable library', async () => { + // add map panel from library + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('Rendering Test: geo map'); + await find.clickByButtonText('Rendering Test: geo map'); + await dashboardAddPanel.closeAddPanel(); + + const originalPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:geomap'); + await panelActions.unlinkFromLibary(originalPanel); + await testSubjects.existOrFail('unlinkPanelSuccess'); + + const updatedPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:geomap'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(false); + + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('Rendering Test: geo map'); + await find.existsByLinkText('Rendering Test: geo map'); + await dashboardAddPanel.closeAddPanel(); + }); + + it('save map panel to embeddable library', async () => { + const originalPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:geomap'); + await panelActions.saveToLibrary('Rendering Test: geo map - copy', originalPanel); + await testSubjects.existOrFail('addPanelToLibrarySuccess'); + + const updatedPanel = await testSubjects.find( + 'embeddablePanelHeading-RenderingTest:geomap-copy' + ); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(true); + }); + }); +} diff --git a/test/functional/apps/dashboard/index.ts b/test/functional/apps/dashboard/index.ts index 9332503539874..b71a89501fbf6 100644 --- a/test/functional/apps/dashboard/index.ts +++ b/test/functional/apps/dashboard/index.ts @@ -81,6 +81,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { // The dashboard_snapshot test below requires the timestamped URL which breaks the view_edit test. // If we don't use the timestamp in the URL, the colors in the charts will be different. loadTestFile(require.resolve('./dashboard_snapshots')); + loadTestFile(require.resolve('./embeddable_library')); }); // Each of these tests call initTests themselves, the way it was originally written. The above tests only load diff --git a/test/functional/config.js b/test/functional/config.js index c15cfffbdb576..05d6cf9dd6b68 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -32,8 +32,10 @@ export default async function ({ readConfigFile }) { servers: commonConfig.get('servers'), - esTestCluster: commonConfig.get('esTestCluster'), - + esTestCluster: { + ...commonConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...commonConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts index 534d4cebd92f4..881e3ad4157a4 100644 --- a/test/functional/services/dashboard/panel_actions.ts +++ b/test/functional/services/dashboard/panel_actions.ts @@ -17,6 +17,8 @@ const TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-togglePanel'; const CUSTOMIZE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-ACTION_CUSTOMIZE_PANEL'; const OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ = 'embeddablePanelToggleMenuIcon'; const OPEN_INSPECTOR_TEST_SUBJ = 'embeddablePanelAction-openInspector'; +const LIBRARY_NOTIFICATION_TEST_SUBJ = 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION'; +const SAVE_TO_LIBRARY_TEST_SUBJ = 'embeddablePanelAction-saveToLibrary'; export function DashboardPanelActionsProvider({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); @@ -170,6 +172,29 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }: Ft await testSubjects.click(OPEN_INSPECTOR_TEST_SUBJ); } + async unlinkFromLibary(parent?: WebElementWrapper) { + log.debug('unlinkFromLibrary'); + const libraryNotification = parent + ? await testSubjects.findDescendant(LIBRARY_NOTIFICATION_TEST_SUBJ, parent) + : await testSubjects.find(LIBRARY_NOTIFICATION_TEST_SUBJ); + await libraryNotification.click(); + await testSubjects.click('libraryNotificationUnlinkButton'); + } + + async saveToLibrary(newTitle: string, parent?: WebElementWrapper) { + log.debug('saveToLibrary'); + await this.openContextMenu(parent); + const exists = await testSubjects.exists(SAVE_TO_LIBRARY_TEST_SUBJ); + if (!exists) { + await this.clickContextMenuMoreItem(); + } + await testSubjects.click(SAVE_TO_LIBRARY_TEST_SUBJ); + await testSubjects.setValue('savedObjectTitle', newTitle, { + clearWithKeyboard: true, + }); + await testSubjects.click('confirmSaveSavedObjectButton'); + } + async expectExistsRemovePanelAction() { log.debug('expectExistsRemovePanelAction'); await this.expectExistsPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ); diff --git a/test/plugin_functional/config.ts b/test/plugin_functional/config.ts index f28e219884bde..bd5ef814ae6c0 100644 --- a/test/plugin_functional/config.ts +++ b/test/plugin_functional/config.ts @@ -36,7 +36,10 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { }, pageObjects: functionalConfig.get('pageObjects'), servers: functionalConfig.get('servers'), - esTestCluster: functionalConfig.get('esTestCluster'), + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, apps: functionalConfig.get('apps'), esArchiver: { directory: path.resolve(__dirname, '../es_archives'), diff --git a/test/server_integration/config.js b/test/server_integration/config.js index 7171a9b33bfd8..0ebb5c48033b8 100644 --- a/test/server_integration/config.js +++ b/test/server_integration/config.js @@ -27,7 +27,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'Integration Tests', }, - esTestCluster: commonConfig.get('esTestCluster'), + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...functionalConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/server_integration/http/ssl/config.js b/test/server_integration/http/ssl/config.js index b305728b64de2..14381de6667fd 100644 --- a/test/server_integration/http/ssl/config.js +++ b/test/server_integration/http/ssl/config.js @@ -33,7 +33,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'Http SSL Integration Tests', }, - esTestCluster: httpConfig.get('esTestCluster'), + esTestCluster: { + ...httpConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...httpConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/server_integration/http/ssl_redirect/config.js b/test/server_integration/http/ssl_redirect/config.js index 0c3e8ce78237a..d19883bcfe241 100644 --- a/test/server_integration/http/ssl_redirect/config.js +++ b/test/server_integration/http/ssl_redirect/config.js @@ -44,7 +44,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'Http SSL Integration Tests', }, - esTestCluster: httpConfig.get('esTestCluster'), + esTestCluster: { + ...httpConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...httpConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/server_integration/http/ssl_with_p12/config.js b/test/server_integration/http/ssl_with_p12/config.js index 75a33226aa669..c4621500e927d 100644 --- a/test/server_integration/http/ssl_with_p12/config.js +++ b/test/server_integration/http/ssl_with_p12/config.js @@ -33,7 +33,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'Http SSL Integration Tests', }, - esTestCluster: httpConfig.get('esTestCluster'), + esTestCluster: { + ...httpConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...httpConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/server_integration/http/ssl_with_p12_intermediate/config.js b/test/server_integration/http/ssl_with_p12_intermediate/config.js index a120ea0b3a556..7f32bad648351 100644 --- a/test/server_integration/http/ssl_with_p12_intermediate/config.js +++ b/test/server_integration/http/ssl_with_p12_intermediate/config.js @@ -33,7 +33,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'Http SSL Integration Tests', }, - esTestCluster: httpConfig.get('esTestCluster'), + esTestCluster: { + ...httpConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...httpConfig.get('kbnTestServer'), serverArgs: [ diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 1eb94af4dddf8..1d50bc7e05807 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -657,7 +657,7 @@ The following table describes the properties of the `incident` object. | externalId | The id of the issue in Jira. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | | issueType | The id of the issue type in Jira. | string _(optional)_ | | priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ | -| labels | An array of labels. | string[] _(optional)_ | +| labels | An array of labels. Labels cannot contain spaces. | string[] _(optional)_ | | parent | The parent issue id or key. Only for `Sub-task` issue types. | string _(optional)_ | #### `subActionParams (getIncident)` diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts index 552053bdd7651..a81dfaeef8175 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts @@ -40,7 +40,15 @@ export const ExecutorSubActionPushParamsSchema = schema.object({ externalId: schema.nullable(schema.string()), issueType: schema.nullable(schema.string()), priority: schema.nullable(schema.string()), - labels: schema.nullable(schema.arrayOf(schema.string())), + labels: schema.nullable( + schema.arrayOf( + schema.string({ + validate: (label) => + // Matches any space, tab or newline character. + label.match(/\s/g) ? `The label ${label} cannot contain spaces` : undefined, + }) + ) + ), parent: schema.nullable(schema.string()), }), comments: schema.nullable( diff --git a/x-pack/plugins/apm/public/application/csmApp.tsx b/x-pack/plugins/apm/public/application/csmApp.tsx index 5fdd45336eb72..8ea4593bb89a7 100644 --- a/x-pack/plugins/apm/public/application/csmApp.tsx +++ b/x-pack/plugins/apm/public/application/csmApp.tsx @@ -11,7 +11,8 @@ import { AppMountParameters, CoreStart } from 'kibana/public'; import React from 'react'; import ReactDOM from 'react-dom'; import { Route, Router } from 'react-router-dom'; -import styled, { DefaultTheme, ThemeProvider } from 'styled-components'; +import { DefaultTheme, ThemeProvider } from 'styled-components'; +import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; import { KibanaContextProvider, RedirectAppLinks, @@ -30,7 +31,7 @@ import { createCallApmApi } from '../services/rest/createCallApmApi'; import { px, units } from '../style/variables'; import { createStaticIndexPattern } from '../services/rest/index_pattern'; -const CsmMainContainer = styled.div` +const CsmMainContainer = euiStyled.div` padding: ${px(units.plus)}; height: 100%; `; diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx index 1996cf3bfe2d9..0028b392fc838 100644 --- a/x-pack/plugins/apm/public/application/index.tsx +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -12,7 +12,8 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Route, Router, Switch } from 'react-router-dom'; import 'react-vis/dist/style.css'; -import styled, { DefaultTheme, ThemeProvider } from 'styled-components'; +import { DefaultTheme, ThemeProvider } from 'styled-components'; +import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; import { ConfigSchema } from '../'; import { AppMountParameters, CoreStart } from '../../../../../src/core/public'; import { @@ -35,7 +36,7 @@ import { createStaticIndexPattern } from '../services/rest/index_pattern'; import { setHelpExtension } from '../setHelpExtension'; import { setReadonlyBadge } from '../updateBadge'; -const MainContainer = styled.div` +const MainContainer = euiStyled.div` height: 100%; `; diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx index ebd15262fd089..cd893c1736988 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx @@ -20,7 +20,7 @@ import { Location } from 'history'; import { first } from 'lodash'; import React from 'react'; import { useHistory } from 'react-router-dom'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; import type { IUrlParams } from '../../../../context/url_params_context/types'; @@ -42,14 +42,14 @@ import { } from './ErrorTabs'; import { ExceptionStacktrace } from './ExceptionStacktrace'; -const HeaderContainer = styled.div` +const HeaderContainer = euiStyled.div` display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: ${px(unit)}; `; -const TransactionLinkName = styled.div` +const TransactionLinkName = euiStyled.div` margin-left: ${px(units.half)}; display: inline-block; vertical-align: middle; diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx index dfc5986f88228..9a8c2dffacaf7 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx @@ -19,7 +19,7 @@ import { import { i18n } from '@kbn/i18n'; import React, { Fragment } from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { useTrackPageview } from '../../../../../observability/public'; import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; import { useFetcher } from '../../../hooks/use_fetcher'; @@ -31,24 +31,24 @@ import { DetailView } from './DetailView'; import { ErrorDistribution } from './Distribution'; import { useErrorGroupDistributionFetcher } from '../../../hooks/use_error_group_distribution_fetcher'; -const Titles = styled.div` +const Titles = euiStyled.div` margin-bottom: ${px(units.plus)}; `; -const Label = styled.div` +const Label = euiStyled.div` margin-bottom: ${px(units.quarter)}; font-size: ${fontSizes.small}; color: ${({ theme }) => theme.eui.euiColorMediumShade}; `; -const Message = styled.div` +const Message = euiStyled.div` font-family: ${fontFamilyCode}; font-weight: bold; font-size: ${fontSizes.large}; margin-bottom: ${px(units.half)}; `; -const Culprit = styled.div` +const Culprit = euiStyled.div` font-family: ${fontFamilyCode}; `; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterBadgeList.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterBadgeList.tsx index 6bc345ea5bd87..785d50de64553 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterBadgeList.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterBadgeList.tsx @@ -7,11 +7,11 @@ import React from 'react'; import { EuiFlexGrid, EuiFlexItem, EuiBadge } from '@elastic/eui'; -import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { unit, px, truncate } from '../../../../../style/variables'; -const BadgeText = styled.div` +const BadgeText = euiStyled.div` display: inline-block; ${truncate(px(unit * 8))}; vertical-align: middle; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterTitleButton.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterTitleButton.tsx index 5d60f7c2aa332..1a59b7d910b1f 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterTitleButton.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterTitleButton.tsx @@ -7,9 +7,9 @@ import React from 'react'; import { EuiButtonEmpty, EuiTitle } from '@elastic/eui'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; -const Button = styled(EuiButtonEmpty).attrs(() => ({ +const Button = euiStyled(EuiButtonEmpty).attrs(() => ({ contentProps: { className: 'alignLeft', }, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/index.tsx index e1debde1117f9..391766a0cf927 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/index.tsx @@ -19,12 +19,12 @@ import { EuiFlexGroup, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { FilterBadgeList } from './FilterBadgeList'; import { unit, px } from '../../../../../style/variables'; import { FilterTitleButton } from './FilterTitleButton'; -const Popover = styled((EuiPopover as unknown) as FunctionComponent).attrs( +const Popover = euiStyled((EuiPopover as unknown) as FunctionComponent).attrs( () => ({ anchorClassName: 'anchor', }) @@ -34,22 +34,22 @@ const Popover = styled((EuiPopover as unknown) as FunctionComponent).attrs( } `; -const SelectContainer = styled.div` +const SelectContainer = euiStyled.div` width: ${px(unit * 16)}; `; -const Counter = styled.div` +const Counter = euiStyled.div` border-radius: ${({ theme }) => theme.eui.euiBorderRadius}; background: ${({ theme }) => theme.eui.euiColorLightShade}; padding: 0 ${({ theme }) => theme.eui.paddingSizes.xs}; `; -const ApplyButton = styled(EuiButton)` +const ApplyButton = euiStyled(EuiButton)` align-self: flex-end; `; // needed for IE11 -const FlexItem = styled(EuiFlexItem)` +const FlexItem = euiStyled(EuiFlexItem)` flex-basis: auto !important; `; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx index a07997fb74921..4afecb7623f73 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx @@ -13,7 +13,7 @@ import { EuiButtonEmpty, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { Filter } from './Filter'; import { useLocalUIFilters } from '../hooks/useLocalUIFilters'; import { LocalUIFilterName } from '../../../../../common/ui_filter'; @@ -26,7 +26,7 @@ interface Props { shouldFetch?: boolean; } -const ButtonWrapper = styled.div` +const ButtonWrapper = euiStyled.div` display: inline-block; `; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx index 9737f6a5e2eba..3362219fd5f2d 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx @@ -8,7 +8,7 @@ import { EuiButtonIcon, EuiPanel, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useContext, useEffect, useState } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { useTheme } from '../../../hooks/use_theme'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; @@ -17,23 +17,23 @@ import { APMQueryParams } from '../../shared/Links/url_helpers'; import { CytoscapeContext } from './Cytoscape'; import { getAnimationOptions, getNodeHeight } from './cytoscape_options'; -const ControlsContainer = styled('div')` +const ControlsContainer = euiStyled('div')` left: ${({ theme }) => theme.eui.gutterTypes.gutterMedium}; position: absolute; top: ${({ theme }) => theme.eui.gutterTypes.gutterSmall}; z-index: 1; /* The element containing the cytoscape canvas has z-index = 0. */ `; -const Button = styled(EuiButtonIcon)` +const Button = euiStyled(EuiButtonIcon)` display: block; margin: ${({ theme }) => theme.eui.paddingSizes.xs}; `; -const ZoomInButton = styled(Button)` +const ZoomInButton = euiStyled(Button)` margin-bottom: ${({ theme }) => theme.eui.paddingSizes.s}; `; -const Panel = styled(EuiPanel)` +const Panel = euiStyled(EuiPanel)` margin-bottom: ${({ theme }) => theme.eui.paddingSizes.s}; `; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx index 0cbf3f013f148..90caa9c87c484 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx @@ -8,12 +8,12 @@ import React, { useContext, useEffect, useState } from 'react'; import { EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; import { CytoscapeContext } from './Cytoscape'; import { useTheme } from '../../../hooks/use_theme'; -const EmptyBannerContainer = styled.div` +const EmptyBannerContainer = euiStyled.div` margin: ${({ theme }) => theme.eui.gutterTypes.gutterSmall}; /* Add some extra margin so it displays to the right of the controls. */ left: calc( diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx index 50b1502a86fd3..c98116a69da66 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, @@ -15,6 +14,7 @@ import { EuiIconTip, EuiHealth, } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { getServiceHealthStatus, getServiceHealthStatusColor, @@ -30,28 +30,28 @@ import { ServiceAnomalyStats, } from '../../../../../common/anomaly_detection'; -const HealthStatusTitle = styled(EuiTitle)` +const HealthStatusTitle = euiStyled(EuiTitle)` display: inline; text-transform: uppercase; `; -const VerticallyCentered = styled.div` +const VerticallyCentered = euiStyled.div` display: flex; align-items: center; `; -const SubduedText = styled.span` +const SubduedText = euiStyled.span` color: ${({ theme }) => theme.eui.euiTextSubduedColor}; `; -const EnableText = styled.section` +const EnableText = euiStyled.section` color: ${({ theme }) => theme.eui.euiTextSubduedColor}; line-height: 1.4; font-size: ${fontSize}; width: ${px(popoverWidth)}; `; -export const ContentLine = styled.section` +export const ContentLine = euiStyled.section` line-height: 2; `; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx index 4900d1dedbde5..9577a02d68cf2 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx @@ -13,24 +13,24 @@ import { import { i18n } from '@kbn/i18n'; import cytoscape from 'cytoscape'; import React, { Fragment } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { SPAN_SUBTYPE, SPAN_TYPE, } from '../../../../../common/elasticsearch_fieldnames'; import { ExternalConnectionNode } from '../../../../../common/service_map'; -const ItemRow = styled.div` +const ItemRow = euiStyled.div` line-height: 2; `; -const SubduedDescriptionListTitle = styled(EuiDescriptionListTitle)` +const SubduedDescriptionListTitle = euiStyled(EuiDescriptionListTitle)` &&& { color: ${({ theme }) => theme.eui.euiTextSubduedColor}; } `; -const ExternalResourcesList = styled.section` +const ExternalResourcesList = euiStyled.section` max-height: 360px; overflow: auto; `; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx index 65508e6adc0ca..766debc6d5587 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { isNumber } from 'lodash'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { asDuration, asPercent, @@ -16,16 +16,16 @@ import { } from '../../../../../common/utils/formatters'; import { ServiceNodeStats } from '../../../../../common/service_map'; -export const ItemRow = styled('tr')` +export const ItemRow = euiStyled('tr')` line-height: 2; `; -export const ItemTitle = styled('td')` +export const ItemTitle = euiStyled('td')` color: ${({ theme }) => theme.eui.euiTextSubduedColor}; padding-right: 1rem; `; -export const ItemDescription = styled('td')` +export const ItemDescription = euiStyled('td')` text-align: right; `; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index 7021575da905e..7ef3cbca3ad2f 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import React, { PropsWithChildren, ReactNode } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { isActivePlatinumLicense } from '../../../../common/license_check'; import { useTrackPageview } from '../../../../../observability/public'; import { @@ -33,7 +33,7 @@ interface ServiceMapProps { serviceName?: string; } -const ServiceMapDatePickerFlexGroup = styled(EuiFlexGroup)` +const ServiceMapDatePickerFlexGroup = euiStyled(EuiFlexGroup)` padding: ${({ theme }) => theme.eui.euiSizeM}; border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; margin: 0; diff --git a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx index 302b815f78715..d0c2b5c598039 100644 --- a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx @@ -8,13 +8,13 @@ import { EuiEmptyPrompt } from '@elastic/eui'; import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { getRedirectToTransactionDetailPageUrl } from './get_redirect_to_transaction_detail_page_url'; import { getRedirectToTracePageUrl } from './get_redirect_to_trace_page_url'; -const CentralizedContainer = styled.div` +const CentralizedContainer = euiStyled.div` height: 100%; display: flex; `; diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/List/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/List/index.tsx index 9d891151e75d2..66fb72975acea 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/List/index.tsx @@ -9,8 +9,8 @@ import { EuiBadge, EuiToolTip } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; -import styled from 'styled-components'; import { EuiIconTip } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { @@ -27,25 +27,25 @@ import { TimestampTooltip } from '../../../shared/TimestampTooltip'; import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink'; import { APMQueryParams } from '../../../shared/Links/url_helpers'; -const GroupIdLink = styled(ErrorDetailLink)` +const GroupIdLink = euiStyled(ErrorDetailLink)` font-family: ${fontFamilyCode}; `; -const MessageAndCulpritCell = styled.div` +const MessageAndCulpritCell = euiStyled.div` ${truncate('100%')}; `; -const ErrorLink = styled(ErrorOverviewLink)` +const ErrorLink = euiStyled(ErrorOverviewLink)` ${truncate('100%')}; `; -const MessageLink = styled(ErrorDetailLink)` +const MessageLink = euiStyled(ErrorDetailLink)` font-family: ${fontFamilyCode}; font-size: ${fontSizes.large}; ${truncate('100%')}; `; -const Culprit = styled.div` +const Culprit = euiStyled.div` font-family: ${fontFamilyCode}; `; diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx index 4506700380390..5287e6699aaee 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx @@ -8,11 +8,11 @@ import { EuiFlexItem, EuiFlexGroup, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; -import styled from 'styled-components'; import { ValuesType } from 'utility-types'; import { orderBy } from 'lodash'; import { EuiIcon } from '@elastic/eui'; import { EuiText } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST, @@ -46,12 +46,12 @@ function formatString(value?: string | null) { return value || NOT_AVAILABLE_LABEL; } -const AppLink = styled(ServiceOrTransactionsOverviewLink)` +const AppLink = euiStyled(ServiceOrTransactionsOverviewLink)` font-size: ${fontSizes.large}; ${truncate('100%')}; `; -const ToolTipWrapper = styled.span` +const ToolTipWrapper = euiStyled.span` width: 100%; .apmServiceList__serviceNameTooltip { width: 100%; diff --git a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx index 5832f2b7d1ac9..21871a17f4b04 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; @@ -39,12 +39,12 @@ const INITIAL_DATA = { containerId: '', }; -const Truncate = styled.span` +const Truncate = euiStyled.span` display: block; ${truncate(px(unit * 12))} `; -const MetadataFlexGroup = styled(EuiFlexGroup)` +const MetadataFlexGroup = euiStyled(EuiFlexGroup)` border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; margin-bottom: ${({ theme }) => theme.eui.paddingSizes.m}; padding: ${({ theme }) => diff --git a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx index 00d184f692e3b..c64bbcb569dde 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup, EuiPage, EuiPanel, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../common/i18n'; import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; import { @@ -26,7 +26,7 @@ const INITIAL_PAGE_SIZE = 25; const INITIAL_SORT_FIELD = 'cpu'; const INITIAL_SORT_DIRECTION = 'desc'; -const ServiceNodeName = styled.div` +const ServiceNodeName = euiStyled.div` ${truncate(px(8 * unit))} `; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_table_container.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_table_container.tsx index 45d34cd304ce7..738ff0d7c735f 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_table_container.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_table_container.tsx @@ -6,7 +6,7 @@ */ import React, { ReactNode } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { useBreakPoints } from '../../../hooks/use_break_points'; /** @@ -24,7 +24,7 @@ const tableHeight = 282; * * Hide the empty message when we don't yet have any items and are still loading. */ -const ServiceOverviewTableContainerDiv = styled.div<{ +const ServiceOverviewTableContainerDiv = euiStyled.div<{ isEmptyAndLoading: boolean; shouldUseMobileLayout: boolean; }>` diff --git a/x-pack/plugins/apm/public/components/app/trace_overview/TraceList.tsx b/x-pack/plugins/apm/public/components/app/trace_overview/TraceList.tsx index cdb82418180ba..774333c35b479 100644 --- a/x-pack/plugins/apm/public/components/app/trace_overview/TraceList.tsx +++ b/x-pack/plugins/apm/public/components/app/trace_overview/TraceList.tsx @@ -8,7 +8,7 @@ import { EuiIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { asMillisecondDuration, asTransactionRate, @@ -23,7 +23,7 @@ import { APIReturnType } from '../../../services/rest/createCallApmApi'; type TraceGroup = APIReturnType<'GET /api/apm/traces'>['items'][0]; -const StyledTransactionLink = styled(TransactionDetailLink)` +const StyledTransactionLink = euiStyled(TransactionDetailLink)` font-size: ${fontSizes.large}; ${truncate('100%')}; `; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx index 2f4c3e3a9d24c..ab3773b2cac2e 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx @@ -8,12 +8,12 @@ import { EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { px, unit } from '../../../../../style/variables'; import { Legend } from '../../../../shared/charts/Legend'; import { IServiceColors } from './Waterfall/waterfall_helpers/waterfall_helpers'; -const Legends = styled.div` +const Legends = euiStyled.div` display: flex; > * { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx index 2812c686d7121..8549f09bba248 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx @@ -6,10 +6,9 @@ */ import { EuiFlyout } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; -import styled from 'styled-components'; - -export const ResponsiveFlyout = styled(EuiFlyout)` +export const ResponsiveFlyout = euiStyled(EuiFlyout)` width: 100%; @media (min-width: 800px) { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx index 3509500d9f429..fda2d595e669d 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx @@ -12,7 +12,7 @@ import React, { Fragment } from 'react'; import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql'; import xcode from 'react-syntax-highlighter/dist/cjs/styles/hljs/xcode'; import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../../../src/plugins/kibana_react/common'; import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; import { borderRadius, @@ -26,7 +26,7 @@ import { TruncateHeightSection } from './TruncateHeightSection'; SyntaxHighlighter.registerLanguage('sql', sql); -const DatabaseStatement = styled.div` +const DatabaseStatement = euiStyled.div` padding: ${px(units.half)} ${px(unit)}; background: ${({ theme }) => tint(0.1, theme.eui.euiColorWarning)}; border-radius: ${borderRadius}; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx index 065dadc6dfd0d..3584309ebb20c 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx @@ -6,9 +6,8 @@ */ import React, { Fragment } from 'react'; -import styled from 'styled-components'; - import { EuiSpacer, EuiTitle } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../../../../../src/plugins/kibana_react/common'; import { borderRadius, fontFamilyCode, @@ -19,7 +18,7 @@ import { } from '../../../../../../../style/variables'; import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; -const ContextUrl = styled.div` +const ContextUrl = euiStyled.div` padding: ${px(units.half)} ${px(unit)}; background: ${({ theme }) => theme.eui.euiColorLightestShade}; border-radius: ${borderRadius}; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx index 401c34ed32436..181fcb91ba3e6 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx @@ -8,10 +8,10 @@ import { EuiIcon, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { Fragment, ReactNode, useEffect, useRef, useState } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../../../src/plugins/kibana_react/common'; import { px, units } from '../../../../../../../style/variables'; -const ToggleButtonContainer = styled.div` +const ToggleButtonContainer = euiStyled.div` margin-top: ${px(units.half)}; user-select: none; `; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx index 35f71676da20e..fe4384e84427f 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx @@ -21,7 +21,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { Fragment } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../../../src/plugins/kibana_react/common'; import { px, units } from '../../../../../../../style/variables'; import { Summary } from '../../../../../../shared/Summary'; import { TimestampTooltip } from '../../../../../../shared/TimestampTooltip'; @@ -72,12 +72,12 @@ function getSpanTypes(span: Span) { }; } -const SpanBadge = (styled(EuiBadge)` +const SpanBadge = euiStyled(EuiBadge)` display: inline-block; margin-right: ${px(units.quarter)}; -` as unknown) as typeof EuiBadge; +`; -const HttpInfoContainer = styled('div')` +const HttpInfoContainer = euiStyled('div')` margin-right: ${px(units.quarter)}; `; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx index cfc90741b0469..24301b2cf10fb 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx @@ -8,13 +8,13 @@ import { EuiBadge } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; import { px, units } from '../../../../../../style/variables'; -const SpanBadge = (styled(EuiBadge)` +const SpanBadge = euiStyled(EuiBadge)` display: inline-block; margin-right: ${px(units.quarter)}; -` as unknown) as typeof EuiBadge; +`; interface SyncBadgeProps { /** diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx index eb34b457d756d..7000f389e3d0e 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx @@ -6,10 +6,10 @@ */ import React, { ReactNode } from 'react'; -import styled from 'styled-components'; import { EuiIcon, EuiText, EuiTitle, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; import { asDuration } from '../../../../../../../common/utils/formatters'; import { isRumAgentName } from '../../../../../../../common/agent_name'; import { px, unit, units } from '../../../../../../style/variables'; @@ -33,7 +33,7 @@ interface IBarStyleProps { color: string; } -const Container = styled.div` +const Container = euiStyled.div` position: relative; display: block; user-select: none; @@ -50,7 +50,7 @@ const Container = styled.div` } `; -const ItemBar = styled.div` +const ItemBar = euiStyled.div` box-sizing: border-box; position: relative; height: ${px(unit)}; @@ -58,7 +58,7 @@ const ItemBar = styled.div` background-color: ${(props) => props.color}; `; -const ItemText = styled.span` +const ItemText = euiStyled.span` position: absolute; right: 0; display: flex; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx index 08bd8c21b7649..8d50074d814eb 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx @@ -9,7 +9,7 @@ import { EuiAccordion, EuiAccordionProps } from '@elastic/eui'; import { Location } from 'history'; import { isEmpty } from 'lodash'; import React, { useState } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; import { Margins } from '../../../../../shared/charts/Timeline'; import { WaterfallItem } from './WaterfallItem'; import { @@ -32,7 +32,7 @@ interface AccordionWaterfallProps { onClickWaterfallItem: (item: IWaterfallItem) => void; } -const StyledAccordion = styled(EuiAccordion).withConfig({ +const StyledAccordion = euiStyled(EuiAccordion).withConfig({ shouldForwardProp: (prop) => !['childrenCount', 'marginLeftLevel', 'hasError'].includes(prop), })< @@ -86,7 +86,7 @@ const StyledAccordion = styled(EuiAccordion).withConfig({ }} `; -const WaterfallItemContainer = styled.div` +const WaterfallItemContainer = euiStyled.div` position: absolute; width: 100%; left: 0; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx index 2ee3b53242a78..a680fdc404402 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { History, Location } from 'history'; import React, { useState } from 'react'; import { useHistory } from 'react-router-dom'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; import { Timeline } from '../../../../../shared/charts/Timeline'; import { HeightRetainer } from '../../../../../shared/HeightRetainer'; import { fromQuery, toQuery } from '../../../../../shared/Links/url_helpers'; @@ -23,7 +23,7 @@ import { IWaterfallItem, } from './waterfall_helpers/waterfall_helpers'; -const Container = styled.div` +const Container = euiStyled.div` transition: 0.1s padding ease; position: relative; overflow: hidden; @@ -55,7 +55,7 @@ const toggleFlyout = ({ }); }; -const WaterfallItemsContainer = styled.div` +const WaterfallItemsContainer = euiStyled.div` border-bottom: 1px solid ${({ theme }) => theme.eui.euiColorMediumShade}; `; diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx index 9a1a691154b18..795a6e66f70a4 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx @@ -8,7 +8,7 @@ import { EuiToolTip, EuiIconTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { @@ -26,7 +26,7 @@ type TransactionGroup = APIReturnType<'GET /api/apm/services/{serviceName}/trans // Truncate both the link and the child span (the tooltip anchor.) The link so // it doesn't overflow, and the anchor so we get the ellipsis. -const TransactionNameLink = styled(TransactionDetailLink)` +const TransactionNameLink = euiStyled(TransactionDetailLink)` font-family: ${fontFamilyCode}; white-space: nowrap; ${truncate('100%')}; diff --git a/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx b/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx index f03c9dd0a2332..414011df7f9ef 100644 --- a/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx @@ -7,13 +7,13 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { ReactNode } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { HeaderMenuPortal } from '../../../../../observability/public'; import { ActionMenu } from '../../../application/action_menu'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { EnvironmentFilter } from '../EnvironmentFilter'; -const HeaderFlexGroup = styled(EuiFlexGroup)` +const HeaderFlexGroup = euiStyled(EuiFlexGroup)` padding: ${({ theme }) => theme.eui.gutterTypes.gutterMedium}; border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; `; diff --git a/x-pack/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx b/x-pack/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx index 6ab4f2e0388b4..ed91aefdfcf9e 100644 --- a/x-pack/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx +++ b/x-pack/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx @@ -7,10 +7,10 @@ import { isBoolean, isNumber, isObject } from 'lodash'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; -const EmptyValue = styled.span` +const EmptyValue = euiStyled.span` color: ${({ theme }) => theme.eui.euiColorMediumShade}; text-align: left; `; diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestion.js b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestion.js index fe767f86239b1..46da6fe4be4c9 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestion.js +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestion.js @@ -7,7 +7,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { EuiIcon } from '@elastic/eui'; import { fontFamilyCode, @@ -33,7 +33,7 @@ function getIconColor(type, theme) { } } -const Description = styled.div` +const Description = euiStyled.div` color: ${({ theme }) => theme.eui.euiColorDarkShade}; p { @@ -48,7 +48,7 @@ const Description = styled.div` } `; -const ListItem = styled.li` +const ListItem = euiStyled.li` font-size: ${fontSizes.small}; height: ${px(units.double)}; align-items: center; @@ -68,7 +68,7 @@ const ListItem = styled.li` } `; -const Icon = styled.div` +const Icon = euiStyled.div` flex: 0 0 ${px(units.double)}; background: ${({ type, theme }) => tint(0.1, getIconColor(type, theme))}; color: ${({ type, theme }) => getIconColor(type, theme)}; @@ -78,7 +78,7 @@ const Icon = styled.div` line-height: ${px(units.double)}; `; -const TextValue = styled.div` +const TextValue = euiStyled.div` flex: 0 0 ${px(unit * 16)}; color: ${({ theme }) => theme.eui.euiColorDarkestShade}; padding: 0 ${px(units.half)}; diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js index ce0fcab5dea1c..cbbf762fa341c 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js @@ -7,13 +7,13 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { isEmpty } from 'lodash'; import Suggestion from './Suggestion'; import { units, px, unit } from '../../../../style/variables'; import { tint } from 'polished'; -const List = styled.ul` +const List = euiStyled.ul` width: 100%; border: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; border-radius: ${px(units.quarter)}; diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx index 98eb0548b8521..efa4f26d9a23f 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { startsWith, uniqueId } from 'lodash'; import React, { useState } from 'react'; import { useHistory, useLocation, useParams } from 'react-router-dom'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { esKuery, IIndexPattern, @@ -24,7 +24,7 @@ import { getBoolFilter } from './get_bool_filter'; import { Typeahead } from './Typeahead'; import { useProcessorEvent } from './use_processor_event'; -const Container = styled.div` +const Container = euiStyled.div` margin-bottom: 10px; `; diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx index 7f8c68ee32ef8..090ba0e8e28cf 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx @@ -6,23 +6,23 @@ */ import React from 'react'; -import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { EuiAccordion, EuiTitle } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { px, unit, units } from '../../../style/variables'; import { Stacktrace } from '.'; import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; -const Accordion = styled(EuiAccordion)` +const Accordion = euiStyled(EuiAccordion)` border-top: ${({ theme }) => theme.eui.euiBorderThin}; margin-top: ${px(units.half)}; `; -const CausedByContainer = styled('h5')` +const CausedByContainer = euiStyled('h5')` padding: ${({ theme }) => theme.eui.spacerSizes.s} 0; `; -const CausedByHeading = styled('span')` +const CausedByHeading = euiStyled('span')` color: ${({ theme }) => theme.eui.euiTextSubduedColor}; display: block; font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; @@ -30,7 +30,7 @@ const CausedByHeading = styled('span')` text-transform: uppercase; `; -const FramesContainer = styled('div')` +const FramesContainer = euiStyled('div')` padding-left: ${px(unit)}; `; diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx index 7a503258b2e58..85d29dda95b5c 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx @@ -13,7 +13,7 @@ import python from 'react-syntax-highlighter/dist/cjs/languages/hljs/python'; import ruby from 'react-syntax-highlighter/dist/cjs/languages/hljs/ruby'; import xcode from 'react-syntax-highlighter/dist/cjs/styles/hljs/xcode'; import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { StackframeWithLineContext } from '../../../../typings/es_schemas/raw/fields/stackframe'; import { borderRadius, px, unit, units } from '../../../style/variables'; @@ -21,13 +21,13 @@ SyntaxHighlighter.registerLanguage('javascript', javascript); SyntaxHighlighter.registerLanguage('python', python); SyntaxHighlighter.registerLanguage('ruby', ruby); -const ContextContainer = styled.div` +const ContextContainer = euiStyled.div` position: relative; border-radius: ${borderRadius}; `; const LINE_HEIGHT = units.eighth * 9; -const LineHighlight = styled.div<{ lineNumber: number }>` +const LineHighlight = euiStyled.div<{ lineNumber: number }>` position: absolute; width: 100%; height: ${px(units.eighth * 9)}; @@ -36,7 +36,7 @@ const LineHighlight = styled.div<{ lineNumber: number }>` background-color: ${({ theme }) => tint(0.1, theme.eui.euiColorWarning)}; `; -const LineNumberContainer = styled.div<{ isLibraryFrame: boolean }>` +const LineNumberContainer = euiStyled.div<{ isLibraryFrame: boolean }>` position: absolute; top: 0; left: 0; @@ -47,7 +47,7 @@ const LineNumberContainer = styled.div<{ isLibraryFrame: boolean }>` : theme.eui.euiColorLightestShade}; `; -const LineNumber = styled.div<{ highlight: boolean }>` +const LineNumber = euiStyled.div<{ highlight: boolean }>` position: relative; min-width: ${px(units.eighth * 21)}; padding-left: ${px(units.half)}; @@ -64,7 +64,7 @@ const LineNumber = styled.div<{ highlight: boolean }>` } `; -const LineContainer = styled.div` +const LineContainer = euiStyled.div` overflow: auto; margin: 0 0 0 ${px(units.eighth * 21)}; padding: 0; @@ -75,7 +75,7 @@ const LineContainer = styled.div` } `; -const Line = styled.pre` +const Line = euiStyled.pre` // Override all styles margin: 0; color: inherit; @@ -87,7 +87,7 @@ const Line = styled.pre` line-height: ${px(LINE_HEIGHT)}; `; -const Code = styled.code` +const Code = euiStyled.code` position: relative; padding: 0; margin: 0; diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx index 636252b19fe39..68b0893e1d8d3 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx @@ -6,7 +6,7 @@ */ import React, { ComponentType } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; import { fontFamilyCode, fontSize, px, units } from '../../../style/variables'; import { @@ -18,7 +18,7 @@ import { RubyFrameHeadingRenderer, } from './frame_heading_renderers'; -const FileDetails = styled.div` +const FileDetails = euiStyled.div` color: ${({ theme }) => theme.eui.euiColorDarkShade}; line-height: 1.5; /* matches the line-hight of the accordion container button */ padding: ${px(units.eighth)} 0; @@ -26,12 +26,12 @@ const FileDetails = styled.div` font-size: ${fontSize}; `; -const LibraryFrameFileDetail = styled.span` +const LibraryFrameFileDetail = euiStyled.span` color: ${({ theme }) => theme.eui.euiColorDarkShade}; word-break: break-word; `; -const AppFrameFileDetail = styled.span` +const AppFrameFileDetail = euiStyled.span` color: ${({ theme }) => theme.eui.euiColorFullShade}; word-break: break-word; `; diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx index e67341d68b52f..de417b465638f 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx @@ -8,12 +8,12 @@ import { EuiAccordion } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; import { px, units } from '../../../style/variables'; import { Stackframe as StackframeComponent } from './Stackframe'; -const LibraryStacktraceAccordion = styled(EuiAccordion)` +const LibraryStacktraceAccordion = euiStyled(EuiAccordion)` margin: ${px(units.quarter)} 0; `; diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx index 4fd90d343146a..d361634759390 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx @@ -7,7 +7,7 @@ import { EuiAccordion } from '@elastic/eui'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { Stackframe as StackframeType, StackframeWithLineContext, @@ -22,7 +22,7 @@ import { FrameHeading } from './FrameHeading'; import { Variables } from './Variables'; import { px, units } from '../../../style/variables'; -const ContextContainer = styled.div<{ isLibraryFrame: boolean }>` +const ContextContainer = euiStyled.div<{ isLibraryFrame: boolean }>` position: relative; font-family: ${fontFamilyCode}; font-size: ${fontSize}; @@ -35,7 +35,7 @@ const ContextContainer = styled.div<{ isLibraryFrame: boolean }>` `; // Indent the non-context frames the same amount as the accordion control -const NoContextFrameHeadingWrapper = styled.div` +const NoContextFrameHeadingWrapper = euiStyled.div` margin-left: ${px(units.unit + units.half + units.quarter)}; `; diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx index 099611d518d55..7c09048593710 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx @@ -5,16 +5,16 @@ * 2.0. */ -import styled from 'styled-components'; import { EuiAccordion } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { borderRadius, px, unit, units } from '../../../style/variables'; import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; import { KeyValueTable } from '../KeyValueTable'; import { flattenObject } from '../../../utils/flattenObject'; -const VariablesContainer = styled.div` +const VariablesContainer = euiStyled.div` background: ${({ theme }) => theme.eui.euiColorEmptyShade}; border-radius: 0 0 ${borderRadius} ${borderRadius}; padding: ${px(units.half)} ${px(unit)}; diff --git a/x-pack/plugins/apm/public/components/shared/StickyProperties/index.tsx b/x-pack/plugins/apm/public/components/shared/StickyProperties/index.tsx index d07b712e83528..ee764db516d72 100644 --- a/x-pack/plugins/apm/public/components/shared/StickyProperties/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/StickyProperties/index.tsx @@ -8,7 +8,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { EuiToolTip } from '@elastic/eui'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { fontFamilyCode, fontSizes, @@ -25,11 +25,11 @@ export interface IStickyProperty { truncated?: boolean; } -const TooltipFieldName = styled.span` +const TooltipFieldName = euiStyled.span` font-family: ${fontFamilyCode}; `; -const PropertyLabel = styled.div` +const PropertyLabel = euiStyled.div` margin-bottom: ${px(units.half)}; font-size: ${fontSizes.small}; color: ${({ theme }) => theme.eui.euiColorMediumShade}; @@ -41,13 +41,13 @@ const PropertyLabel = styled.div` PropertyLabel.displayName = 'PropertyLabel'; const propertyValueLineHeight = 1.2; -const PropertyValue = styled.div` +const PropertyValue = euiStyled.div` display: inline-block; line-height: ${propertyValueLineHeight}; `; PropertyValue.displayName = 'PropertyValue'; -const PropertyValueTruncated = styled.span` +const PropertyValueTruncated = euiStyled.span` display: inline-block; line-height: ${propertyValueLineHeight}; ${truncate('100%')}; diff --git a/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx b/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx index 138afaf256558..ec309f2f74d10 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; import { EuiBadge } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { useTheme } from '../../../hooks/use_theme'; import { px } from '../../../../public/style/variables'; import { units } from '../../../style/variables'; @@ -17,9 +17,9 @@ interface Props { count: number; } -const Badge = (styled(EuiBadge)` +const Badge = euiStyled(EuiBadge)` margin-top: ${px(units.eighth)}; -` as unknown) as typeof EuiBadge; +`; export function ErrorCountSummaryItemBadge({ count }: Props) { const theme = useTheme(); diff --git a/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx b/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx index 9e8242dfa2a7d..d72f03c386226 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx @@ -8,15 +8,15 @@ import React from 'react'; import { EuiToolTip, EuiBadge } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { units, px, truncate, unit } from '../../../../style/variables'; import { HttpStatusBadge } from '../HttpStatusBadge'; -const HttpInfoBadge = (styled(EuiBadge)` +const HttpInfoBadge = euiStyled(EuiBadge)` margin-right: ${px(units.quarter)}; -` as unknown) as typeof EuiBadge; +`; -const Url = styled('span')` +const Url = euiStyled('span')` display: inline-block; vertical-align: bottom; ${truncate(px(unit * 24))}; @@ -27,7 +27,7 @@ interface HttpInfoProps { url: string; } -const Span = styled('span')` +const Span = euiStyled('span')` white-space: nowrap; `; diff --git a/x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx b/x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx index 703b0787f7923..20fd19a06c9eb 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx @@ -6,14 +6,14 @@ */ import React from 'react'; -import styled from 'styled-components'; import { EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { UserAgent } from '../../../../typings/es_schemas/raw/fields/user_agent'; type UserAgentSummaryItemProps = UserAgent; -const Version = styled('span')` +const Version = euiStyled('span')` font-size: ${({ theme }) => theme.eui.euiFontSizeS}; `; diff --git a/x-pack/plugins/apm/public/components/shared/Summary/index.tsx b/x-pack/plugins/apm/public/components/shared/Summary/index.tsx index 357e14ffef356..395156800dceb 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { px, units } from '../../../../public/style/variables'; import { Maybe } from '../../../../typings/common'; @@ -15,7 +15,7 @@ interface Props { items: Array>; } -const Item = styled(EuiFlexItem)` +const Item = euiStyled(EuiFlexItem)` flex-wrap: nowrap; border-right: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; padding-right: ${px(units.half)}; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx index 8ce60b58c4c44..f81da48b760e7 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { useTheme } from '../../../../hooks/use_theme'; import { fontSizes, px, units } from '../../../../style/variables'; @@ -22,7 +22,7 @@ interface ContainerProps { disabled: boolean; } -const Container = styled.div` +const Container = euiStyled.div` display: flex; align-items: center; font-size: ${(props) => props.fontSize}; @@ -39,7 +39,7 @@ interface IndicatorProps { withMargin: boolean; } -export const Indicator = styled.span` +export const Indicator = euiStyled.span` width: ${(props) => px(props.radius)}; height: ${(props) => px(props.radius)}; margin-right: ${(props) => (props.withMargin ? px(props.radius / 2) : 0)}; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx index ad8b85ba70c9b..3b7f0fab6c2a7 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx @@ -7,19 +7,19 @@ import React from 'react'; import { EuiToolTip } from '@elastic/eui'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { asDuration } from '../../../../../../common/utils/formatters'; import { useTheme } from '../../../../../hooks/use_theme'; import { px, units } from '../../../../../style/variables'; import { Legend } from '../../Legend'; import { AgentMark } from '../../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks'; -const NameContainer = styled.div` +const NameContainer = euiStyled.div` border-bottom: 1px solid ${({ theme }) => theme.eui.euiColorMediumShade}; padding-bottom: ${px(units.half)}; `; -const TimeContainer = styled.div` +const TimeContainer = euiStyled.div` color: ${({ theme }) => theme.eui.euiColorMediumShade}; padding-top: ${px(units.half)}; `; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx index 393281b2bf848..044070303d2ff 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import { EuiPopover, EuiText } from '@elastic/eui'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { asDuration } from '../../../../../../common/utils/formatters'; import { useTheme } from '../../../../../hooks/use_theme'; import { @@ -24,21 +24,21 @@ interface Props { mark: ErrorMark; } -const Popover = styled.div` +const Popover = euiStyled.div` max-width: ${px(280)}; `; -const TimeLegend = styled(Legend)` +const TimeLegend = euiStyled(Legend)` margin-bottom: ${px(unit)}; `; -const ErrorLink = styled(ErrorDetailLink)` +const ErrorLink = euiStyled(ErrorDetailLink)` display: block; margin: ${px(units.half)} 0 ${px(units.half)} 0; overflow-wrap: break-word; `; -const Button = styled(Legend)` +const Button = euiStyled(Legend)` height: 20px; display: flex; align-items: flex-end; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx index b426a10a7562d..bece72b398d31 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { px } from '../../../../../style/variables'; import { AgentMarker } from './AgentMarker'; import { ErrorMarker } from './ErrorMarker'; @@ -18,7 +18,7 @@ interface Props { x: number; } -const MarkerContainer = styled.div` +const MarkerContainer = euiStyled.div` position: absolute; bottom: 0; `; diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx index cbadbb0cf4f81..a64355e47f757 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import React from 'react'; import { useParams } from 'react-router-dom'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { MLSingleMetricLink } from '../../Links/MachineLearningLinks/MLSingleMetricLink'; @@ -20,14 +20,14 @@ interface Props { mlJobId?: string; } -const ShiftedIconWrapper = styled.span` +const ShiftedIconWrapper = euiStyled.span` padding-right: 5px; position: relative; top: -1px; display: inline-block; `; -const ShiftedEuiText = styled(EuiText)` +const ShiftedEuiText = euiStyled(EuiText)` position: relative; top: 5px; `; diff --git a/x-pack/plugins/apm/public/components/shared/main_tabs.tsx b/x-pack/plugins/apm/public/components/shared/main_tabs.tsx index de4b368efdbbc..941ce924cff07 100644 --- a/x-pack/plugins/apm/public/components/shared/main_tabs.tsx +++ b/x-pack/plugins/apm/public/components/shared/main_tabs.tsx @@ -7,12 +7,12 @@ import { EuiTabs } from '@elastic/eui'; import React, { ReactNode } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; // Since our `EuiTab` components have `APMLink`s inside of them and not just // `href`s, we need to override the color of the links inside or they will all // be the primary color. -const StyledTabs = styled(EuiTabs)` +const StyledTabs = euiStyled(EuiTabs)` padding: ${({ theme }) => `${theme.eui.gutterTypes.gutterMedium}`}; border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; `; diff --git a/x-pack/plugins/apm/public/components/shared/search_bar.tsx b/x-pack/plugins/apm/public/components/shared/search_bar.tsx index 34ba1d86264c1..3285db1f49191 100644 --- a/x-pack/plugins/apm/public/components/shared/search_bar.tsx +++ b/x-pack/plugins/apm/public/components/shared/search_bar.tsx @@ -7,14 +7,14 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; import { px, unit } from '../../style/variables'; import { DatePicker } from './DatePicker'; import { KueryBar } from './KueryBar'; import { TimeComparison } from './time_comparison'; import { useBreakPoints } from '../../hooks/use_break_points'; -const SearchBarFlexGroup = styled(EuiFlexGroup)` +const SearchBarFlexGroup = euiStyled(EuiFlexGroup)` margin: ${({ theme }) => `${theme.eui.euiSizeS} ${theme.eui.euiSizeS} -${theme.eui.gutterTypes.gutterMedium} ${theme.eui.euiSizeS}`}; `; diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx index 02064ea786fb0..e4b03bd57377a 100644 --- a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx @@ -10,14 +10,14 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; import React from 'react'; import { useHistory } from 'react-router-dom'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { getDateDifference } from '../../../../common/utils/formatters'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { px, unit } from '../../../style/variables'; import * as urlHelpers from '../../shared/Links/url_helpers'; import { useBreakPoints } from '../../../hooks/use_break_points'; -const PrependContainer = styled.div` +const PrependContainer = euiStyled.div` display: flex; justify-content: center; align-items: center; diff --git a/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx b/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx index c6e939de2b064..63e0b84362073 100644 --- a/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx @@ -7,12 +7,12 @@ import { EuiToolTip } from '@elastic/eui'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { truncate } from '../../../style/variables'; const tooltipAnchorClassname = '_apm_truncate_tooltip_anchor_'; -const TooltipWrapper = styled.div` +const TooltipWrapper = euiStyled.div` width: 100%; .${tooltipAnchorClassname} { width: 100% !important; @@ -20,7 +20,7 @@ const TooltipWrapper = styled.div` } `; -const ContentWrapper = styled.div` +const ContentWrapper = euiStyled.div` ${truncate('100%')} `; diff --git a/x-pack/plugins/grokdebugger/public/components/grok_debugger/grok_debugger.js b/x-pack/plugins/grokdebugger/public/components/grok_debugger/grok_debugger.js index aa566b0562802..17a6408298b07 100644 --- a/x-pack/plugins/grokdebugger/public/components/grok_debugger/grok_debugger.js +++ b/x-pack/plugins/grokdebugger/public/components/grok_debugger/grok_debugger.js @@ -129,7 +129,7 @@ export class GrokDebuggerComponent extends React.Component { - + diff --git a/x-pack/plugins/lens/common/constants.ts b/x-pack/plugins/lens/common/constants.ts index 202b80d3d8406..c3e556b167889 100644 --- a/x-pack/plugins/lens/common/constants.ts +++ b/x-pack/plugins/lens/common/constants.ts @@ -6,6 +6,7 @@ */ export const PLUGIN_ID = 'lens'; +export const APP_ID = 'lens'; export const LENS_EMBEDDABLE_TYPE = 'lens'; export const DOC_TYPE = 'lens'; export const NOT_INTERNATIONALIZED_PRODUCT_NAME = 'Lens Visualizations'; diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 7e95479887dbd..0d72a366fa411 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -38,7 +38,7 @@ import { SavedQuery, syncQueryStateWithUrl, } from '../../../../../src/plugins/data/public'; -import { LENS_EMBEDDABLE_TYPE, getFullPath } from '../../common'; +import { LENS_EMBEDDABLE_TYPE, getFullPath, APP_ID } from '../../common'; import { LensAppProps, LensAppServices, LensAppState } from './types'; import { getLensTopNavConfig } from './lens_top_nav'; import { Document } from '../persistence'; @@ -498,7 +498,7 @@ export function App({ isLinkedToOriginatingApp: false, })); // remove editor state so the connection is still broken after reload - stateTransfer.clearEditorState(); + stateTransfer.clearEditorState(APP_ID); redirectTo(newInput.savedObjectId); return; diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 1ff31e5d4bf6b..5869151485a52 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -23,7 +23,7 @@ import { App } from './app'; import { EditorFrameStart } from '../types'; import { addHelpMenuToAppChrome } from '../help_menu_util'; import { LensPluginStartDependencies } from '../plugin'; -import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE } from '../../common'; +import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE, APP_ID } from '../../common'; import { LensEmbeddableInput, LensByReferenceInput, @@ -57,7 +57,7 @@ export async function mountApp( const storage = new Storage(localStorage); const stateTransfer = embeddable?.getStateTransfer(); const historyLocationState = params.history.location.state as HistoryLocationState; - const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(); + const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(APP_ID); const lensServices: LensAppServices = { data, diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 05da76d9fd207..c667ddea06b33 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -40,7 +40,7 @@ import { ACTION_VISUALIZE_FIELD, VISUALIZE_FIELD_TRIGGER, } from '../../../../src/plugins/ui_actions/public'; -import { getEditPath, NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; +import { APP_ID, getEditPath, NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; import { EditorFrameStart } from './types'; import { getLensAliasConfig } from './vis_type_alias'; import { visualizeFieldAction } from './trigger_actions/visualize_field_actions'; @@ -182,7 +182,7 @@ export class LensPlugin { }; core.application.register({ - id: 'lens', + id: APP_ID, title: NOT_INTERNATIONALIZED_PRODUCT_NAME, navLinkStatus: AppNavLinkStatus.hidden, mount: async (params: AppMountParameters) => { diff --git a/x-pack/plugins/maps/public/render_app.tsx b/x-pack/plugins/maps/public/render_app.tsx index ccd30126b67bd..4d1dff9303b0c 100644 --- a/x-pack/plugins/maps/public/render_app.tsx +++ b/x-pack/plugins/maps/public/render_app.tsx @@ -26,6 +26,7 @@ import { } from '../../../../src/plugins/kibana_utils/public'; import { ListPage, MapPage } from './routes'; import { MapByValueInput, MapByReferenceInput } from './embeddable/types'; +import { APP_ID } from '../common/constants'; export let goToSpecifiedPath: (path: string) => void; export let kbnUrlStateStorage: IKbnUrlStateStorage; @@ -80,7 +81,7 @@ export async function renderApp({ function renderMapApp(routeProps: RouteComponentProps<{ savedMapId?: string }>) { const { embeddableId, originatingApp, valueInput } = - stateTransfer.getIncomingEditorState() || {}; + stateTransfer.getIncomingEditorState(APP_ID) || {}; let mapEmbeddableInput; if (routeProps.match.params.savedMapId) { diff --git a/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx b/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx index 66b65eb8d0a9d..feafb34f6a715 100644 --- a/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx +++ b/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { Redirect } from 'react-router-dom'; import { getSavedObjectsClient, getToasts } from '../../kibana_services'; import { MapsListView } from './maps_list_view'; -import { MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; +import { APP_ID, MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; import { EmbeddableStateTransfer } from '../../../../../../src/plugins/embeddable/public'; export class LoadListAndRender extends React.Component<{ stateTransfer: EmbeddableStateTransfer }> { @@ -22,7 +22,7 @@ export class LoadListAndRender extends React.Component<{ stateTransfer: Embeddab componentDidMount() { this._isMounted = true; - this.props.stateTransfer.clearEditorState(); + this.props.stateTransfer.clearEditorState(APP_ID); this._loadMapsList(); } diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts index d38ff8b3e4da6..b6ee5274f690d 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts @@ -9,7 +9,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { EmbeddableStateTransfer } from 'src/plugins/embeddable/public'; import { MapSavedObjectAttributes } from '../../../../common/map_saved_object_type'; -import { MAP_PATH, MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; +import { APP_ID, MAP_PATH, MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; import { createMapStore, MapStore, MapStoreState } from '../../../reducers/store'; import { getTimeFilters, @@ -364,7 +364,7 @@ export class SavedMap { this._originatingApp = undefined; // remove editor state so the connection is still broken after reload - this._getStateTransfer().clearEditorState(); + this._getStateTransfer().clearEditorState(APP_ID); getToasts().addSuccess({ title: i18n.translate('xpack.maps.topNav.saveSuccessMessage', { diff --git a/x-pack/plugins/maps/server/mvt/get_tile.ts b/x-pack/plugins/maps/server/mvt/get_tile.ts index 3116838d26fb5..50c2014275a0f 100644 --- a/x-pack/plugins/maps/server/mvt/get_tile.ts +++ b/x-pack/plugins/maps/server/mvt/get_tile.ts @@ -25,7 +25,7 @@ import { import { convertRegularRespToGeoJson, hitsToGeoJson } from '../../common/elasticsearch_util'; import { flattenHit } from './util'; -import { ESBounds, tile2lat, tile2long, tileToESBbox } from '../../common/geo_tile_utils'; +import { ESBounds, tileToESBbox } from '../../common/geo_tile_utils'; import { getCentroidFeatures } from '../../common/get_centroid_features'; export async function getGridTile({ @@ -53,35 +53,14 @@ export async function getGridTile({ geoFieldType: ES_GEO_FIELD_TYPE; searchSessionId?: string; }): Promise { - const esBbox: ESBounds = tileToESBbox(x, y, z); try { - let bboxFilter; - if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT) { - bboxFilter = { - geo_bounding_box: { - [geometryFieldName]: esBbox, - }, - }; - } else if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_SHAPE) { - const geojsonPolygon = tileToGeoJsonPolygon(x, y, z); - bboxFilter = { - geo_shape: { - [geometryFieldName]: { - shape: geojsonPolygon, - relation: 'INTERSECTS', - }, - }, - }; - } else { - throw new Error(`${geoFieldType} is not valid geo field-type`); - } - requestBody.query.bool.filter.push(bboxFilter); - + const tileBounds: ESBounds = tileToESBbox(x, y, z); + requestBody.query.bool.filter.push(getTileSpatialFilter(geometryFieldName, tileBounds)); requestBody.aggs[GEOTILE_GRID_AGG_NAME].geotile_grid.precision = Math.min( z + SUPER_FINE_ZOOM_DELTA, MAX_ZOOM ); - requestBody.aggs[GEOTILE_GRID_AGG_NAME].geotile_grid.bounds = esBbox; + requestBody.aggs[GEOTILE_GRID_AGG_NAME].geotile_grid.bounds = tileBounds; const response = await context .search!.search( @@ -134,14 +113,9 @@ export async function getTile({ }): Promise { let features: Feature[]; try { - requestBody.query.bool.filter.push({ - geo_shape: { - [geometryFieldName]: { - shape: tileToGeoJsonPolygon(x, y, z), - relation: 'INTERSECTS', - }, - }, - }); + requestBody.query.bool.filter.push( + getTileSpatialFilter(geometryFieldName, tileToESBbox(x, y, z)) + ); const searchOptions = { sessionId: searchSessionId, @@ -193,7 +167,8 @@ export async function getTile({ [KBN_TOO_MANY_FEATURES_PROPERTY]: true, }, geometry: esBboxToGeoJsonPolygon( - bboxResponse.rawResponse.aggregations.data_bounds.bounds + bboxResponse.rawResponse.aggregations.data_bounds.bounds, + tileToESBbox(x, y, z) ), }, ]; @@ -244,32 +219,31 @@ export async function getTile({ } } -function tileToGeoJsonPolygon(x: number, y: number, z: number): Polygon { - const wLon = tile2long(x, z); - const sLat = tile2lat(y + 1, z); - const eLon = tile2long(x + 1, z); - const nLat = tile2lat(y, z); - +function getTileSpatialFilter(geometryFieldName: string, tileBounds: ESBounds): unknown { return { - type: 'Polygon', - coordinates: [ - [ - [wLon, sLat], - [wLon, nLat], - [eLon, nLat], - [eLon, sLat], - [wLon, sLat], - ], - ], + geo_shape: { + [geometryFieldName]: { + shape: { + type: 'envelope', + // upper left and lower right points of the shape to represent a bounding rectangle in the format [[minLon, maxLat], [maxLon, minLat]] + coordinates: [ + [tileBounds.top_left.lon, tileBounds.top_left.lat], + [tileBounds.bottom_right.lon, tileBounds.bottom_right.lat], + ], + }, + relation: 'INTERSECTS', + }, + }, }; } -function esBboxToGeoJsonPolygon(esBounds: ESBounds): Polygon { - let minLon = esBounds.top_left.lon; - const maxLon = esBounds.bottom_right.lon; +function esBboxToGeoJsonPolygon(esBounds: ESBounds, tileBounds: ESBounds): Polygon { + // Intersecting geo_shapes may push bounding box outside of tile so need to clamp to tile bounds. + let minLon = Math.max(esBounds.top_left.lon, tileBounds.top_left.lon); + const maxLon = Math.min(esBounds.bottom_right.lon, tileBounds.bottom_right.lon); minLon = minLon > maxLon ? minLon - 360 : minLon; // fixes an ES bbox to straddle dateline - const minLat = esBounds.bottom_right.lat; - const maxLat = esBounds.top_left.lat; + const minLat = Math.max(esBounds.bottom_right.lat, tileBounds.bottom_right.lat); + const maxLat = Math.min(esBounds.top_left.lat, tileBounds.top_left.lat); return { type: 'Polygon', diff --git a/x-pack/plugins/monitoring/common/types/alerts.ts b/x-pack/plugins/monitoring/common/types/alerts.ts index 7fb41ece527a1..649b92cb7ac82 100644 --- a/x-pack/plugins/monitoring/common/types/alerts.ts +++ b/x-pack/plugins/monitoring/common/types/alerts.ts @@ -6,7 +6,12 @@ */ import { Alert, AlertTypeParams, SanitizedAlert } from '../../../alerts/common'; -import { AlertParamType, AlertMessageTokenType, AlertSeverity } from '../enums'; +import { + AlertParamType, + AlertMessageTokenType, + AlertSeverity, + AlertClusterHealthType, +} from '../enums'; export type CommonAlert = Alert | SanitizedAlert; @@ -60,6 +65,8 @@ export interface AlertInstanceState { | AlertDiskUsageState | AlertThreadPoolRejectionsState | AlertNodeState + | AlertLicenseState + | AlertNodesChangedState >; [x: string]: unknown; } @@ -74,6 +81,7 @@ export interface AlertState { export interface AlertNodeState extends AlertState { nodeId: string; nodeName?: string; + meta: any; [key: string]: unknown; } @@ -96,6 +104,14 @@ export interface AlertThreadPoolRejectionsState extends AlertState { nodeName?: string; } +export interface AlertLicenseState extends AlertState { + expiryDateMS: number; +} + +export interface AlertNodesChangedState extends AlertState { + node: AlertClusterStatsNode; +} + export interface AlertUiState { isFiring: boolean; resolvedMS?: number; @@ -228,3 +244,36 @@ export interface LegacyAlertNodesChangedList { added: { [nodeName: string]: string }; restarted: { [nodeName: string]: string }; } + +export interface AlertLicense { + status: string; + type: string; + expiryDateMS: number; + clusterUuid: string; + ccs?: string; +} + +export interface AlertClusterStatsNodes { + clusterUuid: string; + recentNodes: AlertClusterStatsNode[]; + priorNodes: AlertClusterStatsNode[]; + ccs?: string; +} + +export interface AlertClusterStatsNode { + nodeUuid: string; + nodeEphemeralId?: string; + nodeName?: string; +} + +export interface AlertClusterHealth { + health: AlertClusterHealthType; + clusterUuid: string; + ccs?: string; +} + +export interface AlertVersions { + clusterUuid: string; + ccs?: string; + versions: string[]; +} diff --git a/x-pack/plugins/monitoring/common/types/es.ts b/x-pack/plugins/monitoring/common/types/es.ts index cb3d44d0080ed..9dce32211f4b1 100644 --- a/x-pack/plugins/monitoring/common/types/es.ts +++ b/x-pack/plugins/monitoring/common/types/es.ts @@ -154,7 +154,10 @@ export interface ElasticsearchLegacySource { cluster_state?: { status?: string; nodes?: { - [nodeUuid: string]: {}; + [nodeUuid: string]: { + ephemeral_id?: string; + name?: string; + }; }; master_node?: boolean; }; @@ -170,6 +173,7 @@ export interface ElasticsearchLegacySource { license?: { status?: string; type?: string; + expiry_date_in_millis?: number; }; logstash_state?: { pipeline?: { diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts index 200c61b29b2e0..e79eb78f7f66b 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -26,26 +26,16 @@ import { AlertEnableAction, CommonAlertFilter, CommonAlertParams, - LegacyAlert, } from '../../common/types/alerts'; import { fetchAvailableCcs } from '../lib/alerts/fetch_available_ccs'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; -import { INDEX_PATTERN_ELASTICSEARCH, INDEX_ALERTS } from '../../common/constants'; +import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; -import { MonitoringLicenseService } from '../types'; import { mbSafeQuery } from '../lib/mb_safe_query'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { parseDuration } from '../../../alerts/common/parse_duration'; import { Globals } from '../static_globals'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; -import { mapLegacySeverity } from '../lib/alerts/map_legacy_severity'; - -interface LegacyOptions { - watchName: string; - nodeNameLabel: string; - changeDataValues?: Partial; -} type ExecutedState = | { @@ -60,7 +50,6 @@ interface AlertOptions { name: string; throttle?: string | null; interval?: string; - legacy?: LegacyOptions; defaultParams?: Partial; actionVariables: Array<{ name: string; description: string }>; fetchClustersRange?: number; @@ -126,16 +115,6 @@ export class BaseAlert { }; } - public isEnabled(licenseService: MonitoringLicenseService) { - if (this.alertOptions.legacy) { - const watcherFeature = licenseService.getWatcherFeature(); - if (!watcherFeature.isAvailable || !watcherFeature.isEnabled) { - return false; - } - } - return true; - } - public getId() { return this.rawAlert?.id; } @@ -271,10 +250,6 @@ export class BaseAlert { params as CommonAlertParams, availableCcs ); - if (this.alertOptions.legacy) { - const data = await this.fetchLegacyData(callCluster, clusters, availableCcs); - return await this.processLegacyData(data, clusters, services, state); - } const data = await this.fetchData(params, callCluster, clusters, availableCcs); return await this.processData(data, clusters, services, state); } @@ -312,35 +287,6 @@ export class BaseAlert { throw new Error('Child classes must implement `fetchData`'); } - protected async fetchLegacyData( - callCluster: CallCluster, - clusters: AlertCluster[], - availableCcs: string[] - ): Promise { - let alertIndexPattern = INDEX_ALERTS; - if (availableCcs) { - alertIndexPattern = getCcsIndexPattern(alertIndexPattern, availableCcs); - } - const legacyAlerts = await fetchLegacyAlerts( - callCluster, - clusters, - alertIndexPattern, - this.alertOptions.legacy!.watchName, - Globals.app.config.ui.max_bucket_size - ); - - return legacyAlerts.map((legacyAlert) => { - return { - clusterUuid: legacyAlert.metadata.cluster_uuid, - shouldFire: !legacyAlert.resolved_timestamp, - severity: mapLegacySeverity(legacyAlert.metadata.severity), - meta: legacyAlert, - nodeName: this.alertOptions.legacy!.nodeNameLabel, - ...this.alertOptions.legacy!.changeDataValues, - }; - }); - } - protected async processData( data: AlertData[], clusters: AlertCluster[], @@ -395,34 +341,6 @@ export class BaseAlert { return state; } - protected async processLegacyData( - data: AlertData[], - clusters: AlertCluster[], - services: AlertServices, - state: ExecutedState - ) { - const currentUTC = +new Date(); - for (const item of data) { - const instanceId = `${this.alertOptions.id}:${item.clusterUuid}`; - const instance = services.alertInstanceFactory(instanceId); - if (!item.shouldFire) { - instance.replaceState({ alertStates: [] }); - continue; - } - const cluster = clusters.find((c: AlertCluster) => c.clusterUuid === item.clusterUuid); - const alertState: AlertState = this.getDefaultAlertState(cluster!, item); - alertState.nodeName = item.nodeName; - alertState.ui.triggeredMS = currentUTC; - alertState.ui.isFiring = true; - alertState.ui.severity = item.severity; - alertState.ui.message = this.getUiMessage(alertState, item); - instance.replaceState({ alertStates: [alertState] }); - this.executeActions(instance, alertState, item, cluster); - } - state.lastChecked = currentUTC; - return state; - } - protected getDefaultAlertState(cluster: AlertCluster, item: AlertData): AlertState { return { cluster, @@ -437,10 +355,6 @@ export class BaseAlert { }; } - protected getVersions(legacyAlert: LegacyAlert) { - return `[${legacyAlert.message.match(/(?<=Versions: \[).+?(?=\])/)}]`; - } - protected getUiMessage( alertState: AlertState | unknown, item: AlertData | unknown diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts index 3d8000d317526..1490a6ce58e04 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts @@ -7,7 +7,8 @@ import { ClusterHealthAlert } from './cluster_health_alert'; import { ALERT_CLUSTER_HEALTH } from '../../common/constants'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { AlertClusterHealthType, AlertSeverity } from '../../common/enums'; +import { fetchClusterHealth } from '../lib/alerts/fetch_cluster_health'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; const RealDate = Date; @@ -26,8 +27,8 @@ jest.mock('../static_globals', () => ({ }, })); -jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ - fetchLegacyAlerts: jest.fn(), +jest.mock('../lib/alerts/fetch_cluster_health', () => ({ + fetchClusterHealth: jest.fn(), })); jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), @@ -63,16 +64,16 @@ describe('ClusterHealthAlert', () => { function FakeDate() {} FakeDate.prototype.valueOf = () => 1; + const ccs = undefined; const clusterUuid = 'abc123'; const clusterName = 'testCluster'; - const legacyAlert = { - prefix: 'Elasticsearch cluster status is yellow.', - message: 'Allocate missing replica shards.', - metadata: { - severity: 2000, - cluster_uuid: clusterUuid, + const healths = [ + { + health: AlertClusterHealthType.Yellow, + clusterUuid, + ccs, }, - }; + ]; const replaceState = jest.fn(); const scheduleActions = jest.fn(); @@ -94,8 +95,8 @@ describe('ClusterHealthAlert', () => { beforeEach(() => { // @ts-ignore Date = FakeDate; - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return [legacyAlert]; + (fetchClusterHealth as jest.Mock).mockImplementation(() => { + return healths; }); (fetchClusters as jest.Mock).mockImplementation(() => { return [{ clusterUuid, clusterName }]; @@ -120,8 +121,15 @@ describe('ClusterHealthAlert', () => { alertStates: [ { cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, - ccs: undefined, - nodeName: 'Elasticsearch cluster alert', + ccs, + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + health: AlertClusterHealthType.Yellow, + }, ui: { isFiring: true, message: { @@ -140,7 +148,7 @@ describe('ClusterHealthAlert', () => { }, ], }, - severity: 'danger', + severity: AlertSeverity.Warning, triggeredMS: 1, lastCheckedMS: 0, }, @@ -160,9 +168,15 @@ describe('ClusterHealthAlert', () => { }); }); - it('should not fire actions if there is no legacy alert', async () => { - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return []; + it('should not fire actions if the cluster health is green', async () => { + (fetchClusterHealth as jest.Mock).mockImplementation(() => { + return [ + { + health: AlertClusterHealthType.Green, + clusterUuid, + ccs, + }, + ]; }); const alert = new ClusterHealthAlert(); const type = alert.getAlertType(); diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts index 63f658d5b0283..c4e5de3d55356 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts @@ -13,13 +13,23 @@ import { AlertState, AlertMessage, AlertMessageLinkToken, - LegacyAlert, + CommonAlertParams, + AlertClusterHealth, + AlertInstanceState, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { ALERT_CLUSTER_HEALTH, LEGACY_ALERT_DETAILS } from '../../common/constants'; -import { AlertMessageTokenType, AlertClusterHealthType } from '../../common/enums'; +import { + ALERT_CLUSTER_HEALTH, + LEGACY_ALERT_DETAILS, + INDEX_PATTERN_ELASTICSEARCH, +} from '../../common/constants'; +import { AlertMessageTokenType, AlertClusterHealthType, AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerts/common'; +import { Globals } from '../static_globals'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { fetchClusterHealth } from '../lib/alerts/fetch_cluster_health'; const RED_STATUS_MESSAGE = i18n.translate('xpack.monitoring.alerts.clusterHealth.redMessage', { defaultMessage: 'Allocate missing primary and replica shards', @@ -37,12 +47,6 @@ export class ClusterHealthAlert extends BaseAlert { super(rawAlert, { id: ALERT_CLUSTER_HEALTH, name: LEGACY_ALERT_DETAILS[ALERT_CLUSTER_HEALTH].label, - legacy: { - watchName: 'elasticsearch_cluster_status', - nodeNameLabel: i18n.translate('xpack.monitoring.alerts.clusterHealth.nodeNameLabel', { - defaultMessage: 'Elasticsearch cluster alert', - }), - }, actionVariables: [ { name: 'clusterHealth', @@ -58,15 +62,36 @@ export class ClusterHealthAlert extends BaseAlert { }); } - private getHealth(legacyAlert: LegacyAlert) { - return legacyAlert.prefix - .replace('Elasticsearch cluster status is ', '') - .slice(0, -1) as AlertClusterHealthType; + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + availableCcs: string[] + ): Promise { + let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); + if (availableCcs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + const healths = await fetchClusterHealth(callCluster, clusters, esIndexPattern); + return healths.map((clusterHealth) => { + const shouldFire = clusterHealth.health !== AlertClusterHealthType.Green; + const severity = + clusterHealth.health === AlertClusterHealthType.Red + ? AlertSeverity.Danger + : AlertSeverity.Warning; + + return { + shouldFire, + severity, + meta: clusterHealth, + clusterUuid: clusterHealth.clusterUuid, + ccs: clusterHealth.ccs, + }; + }); } protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const legacyAlert = item.meta as LegacyAlert; - const health = this.getHealth(legacyAlert); + const { health } = item.meta as AlertClusterHealth; return { text: i18n.translate('xpack.monitoring.alerts.clusterHealth.ui.firingMessage', { defaultMessage: `Elasticsearch cluster health is {health}.`, @@ -98,52 +123,56 @@ export class ClusterHealthAlert extends BaseAlert { protected async executeActions( instance: AlertInstance, - alertState: AlertState, - item: AlertData, + { alertStates }: AlertInstanceState, + item: AlertData | null, cluster: AlertCluster ) { - const legacyAlert = item.meta as LegacyAlert; - const health = this.getHealth(legacyAlert); - if (alertState.ui.isFiring) { - const actionText = - health === AlertClusterHealthType.Red - ? i18n.translate('xpack.monitoring.alerts.clusterHealth.action.danger', { - defaultMessage: `Allocate missing primary and replica shards.`, - }) - : i18n.translate('xpack.monitoring.alerts.clusterHealth.action.warning', { - defaultMessage: `Allocate missing replica shards.`, - }); - - const action = `[${actionText}](elasticsearch/indices)`; - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.clusterHealth.firing.internalShortMessage', - { - defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {actionText}`, - values: { - clusterName: cluster.clusterName, - health, - actionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.clusterHealth.firing.internalFullMessage', - { - defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {action}`, - values: { - clusterName: cluster.clusterName, - health, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - clusterHealth: health, - clusterName: cluster.clusterName, - action, - actionPlain: actionText, - }); + if (alertStates.length === 0) { + return; } + + // Logic in the base alert assumes that all alerts will operate against multiple nodes/instances (such as a CPU alert against ES nodes) + // However, some alerts operate on the state of the cluster itself and are only concerned with a single state + const state = alertStates[0]; + const { health } = state.meta as AlertClusterHealth; + const actionText = + health === AlertClusterHealthType.Red + ? i18n.translate('xpack.monitoring.alerts.clusterHealth.action.danger', { + defaultMessage: `Allocate missing primary and replica shards.`, + }) + : i18n.translate('xpack.monitoring.alerts.clusterHealth.action.warning', { + defaultMessage: `Allocate missing replica shards.`, + }); + + const action = `[${actionText}](elasticsearch/indices)`; + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.firing.internalShortMessage', + { + defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {actionText}`, + values: { + clusterName: cluster.clusterName, + health, + actionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.firing.internalFullMessage', + { + defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {action}`, + values: { + clusterName: cluster.clusterName, + health, + action, + }, + } + ), + state: AlertingDefaults.ALERT_STATE.firing, + clusterHealth: health, + clusterName: cluster.clusterName, + action, + actionPlain: actionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts index 5f9ea3a18b570..a231cec762191 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts @@ -7,13 +7,13 @@ import { ElasticsearchVersionMismatchAlert } from './elasticsearch_version_mismatch_alert'; import { ALERT_ELASTICSEARCH_VERSION_MISMATCH } from '../../common/constants'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchElasticsearchVersions } from '../lib/alerts/fetch_elasticsearch_versions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; const RealDate = Date; -jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ - fetchLegacyAlerts: jest.fn(), +jest.mock('../lib/alerts/fetch_elasticsearch_versions', () => ({ + fetchElasticsearchVersions: jest.fn(), })); jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), @@ -22,6 +22,7 @@ jest.mock('../lib/alerts/fetch_clusters', () => ({ jest.mock('../static_globals', () => ({ Globals: { app: { + url: 'UNIT_TEST_URL', getLogger: () => ({ debug: jest.fn() }), config: { ui: { @@ -67,16 +68,16 @@ describe('ElasticsearchVersionMismatchAlert', () => { function FakeDate() {} FakeDate.prototype.valueOf = () => 1; + const ccs = undefined; const clusterUuid = 'abc123'; const clusterName = 'testCluster'; - const legacyAlert = { - prefix: 'This cluster is running with multiple versions of Elasticsearch.', - message: 'Versions: [8.0.0, 7.2.1].', - metadata: { - severity: 1000, - cluster_uuid: clusterUuid, + const elasticsearchVersions = [ + { + versions: ['8.0.0', '7.2.1'], + clusterUuid, + ccs, }, - }; + ]; const replaceState = jest.fn(); const scheduleActions = jest.fn(); @@ -98,8 +99,8 @@ describe('ElasticsearchVersionMismatchAlert', () => { beforeEach(() => { // @ts-ignore Date = FakeDate; - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return [legacyAlert]; + (fetchElasticsearchVersions as jest.Mock).mockImplementation(() => { + return elasticsearchVersions; }); (fetchClusters as jest.Mock).mockImplementation(() => { return [{ clusterUuid, clusterName }]; @@ -125,13 +126,19 @@ describe('ElasticsearchVersionMismatchAlert', () => { alertStates: [ { cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, - ccs: undefined, - nodeName: 'Elasticsearch node alert', + ccs, + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + versions: ['8.0.0', '7.2.1'], + }, ui: { isFiring: true, message: { - text: - 'Multiple versions of Elasticsearch ([8.0.0, 7.2.1]) running in this cluster.', + text: 'Multiple versions of Elasticsearch (8.0.0, 7.2.1) running in this cluster.', }, severity: 'warning', triggeredMS: 1, @@ -141,21 +148,26 @@ describe('ElasticsearchVersionMismatchAlert', () => { ], }); expect(scheduleActions).toHaveBeenCalledWith('default', { - action: '[View nodes](elasticsearch/nodes)', + action: `[View nodes](UNIT_TEST_URL/app/monitoring#/elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid}))`, actionPlain: 'Verify you have the same version across all nodes.', - internalFullMessage: - 'Elasticsearch version mismatch alert is firing for testCluster. Elasticsearch is running [8.0.0, 7.2.1]. [View nodes](elasticsearch/nodes)', + internalFullMessage: `Elasticsearch version mismatch alert is firing for testCluster. Elasticsearch is running 8.0.0, 7.2.1. [View nodes](UNIT_TEST_URL/app/monitoring#/elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid}))`, internalShortMessage: 'Elasticsearch version mismatch alert is firing for testCluster. Verify you have the same version across all nodes.', - versionList: '[8.0.0, 7.2.1]', + versionList: ['8.0.0', '7.2.1'], clusterName, state: 'firing', }); }); - it('should not fire actions if there is no legacy alert', async () => { - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return []; + it('should not fire actions if there is no mismatch', async () => { + (fetchElasticsearchVersions as jest.Mock).mockImplementation(() => { + return [ + { + versions: ['8.0.0'], + clusterUuid, + ccs, + }, + ]; }); const alert = new ElasticsearchVersionMismatchAlert(); const type = alert.getAlertType(); diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts index 717d803084c6f..e8e93e4b3afec 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts @@ -12,29 +12,29 @@ import { AlertCluster, AlertState, AlertMessage, - LegacyAlert, + AlertInstanceState, + CommonAlertParams, + AlertVersions, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { ALERT_ELASTICSEARCH_VERSION_MISMATCH, LEGACY_ALERT_DETAILS } from '../../common/constants'; +import { + ALERT_ELASTICSEARCH_VERSION_MISMATCH, + LEGACY_ALERT_DETAILS, + INDEX_PATTERN_ELASTICSEARCH, +} from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerts/common'; +import { Globals } from '../static_globals'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { fetchElasticsearchVersions } from '../lib/alerts/fetch_elasticsearch_versions'; export class ElasticsearchVersionMismatchAlert extends BaseAlert { constructor(public rawAlert?: SanitizedAlert) { super(rawAlert, { id: ALERT_ELASTICSEARCH_VERSION_MISMATCH, name: LEGACY_ALERT_DETAILS[ALERT_ELASTICSEARCH_VERSION_MISMATCH].label, - legacy: { - watchName: 'elasticsearch_version_mismatch', - nodeNameLabel: i18n.translate( - 'xpack.monitoring.alerts.elasticsearchVersionMismatch.nodeNameLabel', - { - defaultMessage: 'Elasticsearch node alert', - } - ), - changeDataValues: { severity: AlertSeverity.Warning }, - }, interval: '1d', actionVariables: [ { @@ -51,15 +51,42 @@ export class ElasticsearchVersionMismatchAlert extends BaseAlert { }); } + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + availableCcs: string[] + ): Promise { + let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); + if (availableCcs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + const elasticsearchVersions = await fetchElasticsearchVersions( + callCluster, + clusters, + esIndexPattern, + Globals.app.config.ui.max_bucket_size + ); + + return elasticsearchVersions.map((elasticsearchVersion) => { + return { + shouldFire: elasticsearchVersion.versions.length > 1, + severity: AlertSeverity.Warning, + meta: elasticsearchVersion, + clusterUuid: elasticsearchVersion.clusterUuid, + ccs: elasticsearchVersion.ccs, + }; + }); + } + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const legacyAlert = item.meta as LegacyAlert; - const versions = this.getVersions(legacyAlert); + const { versions } = item.meta as AlertVersions; const text = i18n.translate( 'xpack.monitoring.alerts.elasticsearchVersionMismatch.ui.firingMessage', { defaultMessage: `Multiple versions of Elasticsearch ({versions}) running in this cluster.`, values: { - versions, + versions: versions.join(', '), }, } ); @@ -71,54 +98,63 @@ export class ElasticsearchVersionMismatchAlert extends BaseAlert { protected async executeActions( instance: AlertInstance, - alertState: AlertState, - item: AlertData, + { alertStates }: AlertInstanceState, + item: AlertData | null, cluster: AlertCluster ) { - const legacyAlert = item.meta as LegacyAlert; - const versions = this.getVersions(legacyAlert); - if (alertState.ui.isFiring) { - const shortActionText = i18n.translate( - 'xpack.monitoring.alerts.elasticsearchVersionMismatch.shortAction', + if (alertStates.length === 0) { + return; + } + + // Logic in the base alert assumes that all alerts will operate against multiple nodes/instances (such as a CPU alert against ES nodes) + // However, some alerts operate on the state of the cluster itself and are only concerned with a single state + const state = alertStates[0]; + const { versions } = state.meta as AlertVersions; + const shortActionText = i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.shortAction', + { + defaultMessage: 'Verify you have the same version across all nodes.', + } + ); + const fullActionText = i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.fullAction', + { + defaultMessage: 'View nodes', + } + ); + const globalStateLink = this.createGlobalStateLink( + 'elasticsearch/nodes', + cluster.clusterUuid, + state.ccs + ); + const action = `[${fullActionText}](${globalStateLink})`; + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage', { - defaultMessage: 'Verify you have the same version across all nodes.', + defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, } - ); - const fullActionText = i18n.translate( - 'xpack.monitoring.alerts.elasticsearchVersionMismatch.fullAction', + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalFullMessage', { - defaultMessage: 'View nodes', + defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. Elasticsearch is running {versions}. {action}`, + values: { + clusterName: cluster.clusterName, + versions: versions.join(', '), + action, + }, } - ); - const action = `[${fullActionText}](elasticsearch/nodes)`; - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage', - { - defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. {shortActionText}`, - values: { - clusterName: cluster.clusterName, - shortActionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalFullMessage', - { - defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. Elasticsearch is running {versions}. {action}`, - values: { - clusterName: cluster.clusterName, - versions, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - clusterName: cluster.clusterName, - versionList: versions, - action, - actionPlain: shortActionText, - }); - } + ), + state: AlertingDefaults.ALERT_STATE.firing, + clusterName: cluster.clusterName, + versionList: versions, + action, + actionPlain: shortActionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts index a6cc7445cb764..6252fc59ba246 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts @@ -7,13 +7,13 @@ import { KibanaVersionMismatchAlert } from './kibana_version_mismatch_alert'; import { ALERT_KIBANA_VERSION_MISMATCH } from '../../common/constants'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchKibanaVersions } from '../lib/alerts/fetch_kibana_versions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; const RealDate = Date; -jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ - fetchLegacyAlerts: jest.fn(), +jest.mock('../lib/alerts/fetch_kibana_versions', () => ({ + fetchKibanaVersions: jest.fn(), })); jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), @@ -22,6 +22,7 @@ jest.mock('../lib/alerts/fetch_clusters', () => ({ jest.mock('../static_globals', () => ({ Globals: { app: { + url: 'UNIT_TEST_URL', getLogger: () => ({ debug: jest.fn() }), config: { ui: { @@ -70,16 +71,16 @@ describe('KibanaVersionMismatchAlert', () => { function FakeDate() {} FakeDate.prototype.valueOf = () => 1; + const ccs = undefined; const clusterUuid = 'abc123'; const clusterName = 'testCluster'; - const legacyAlert = { - prefix: 'This cluster is running with multiple versions of Kibana.', - message: 'Versions: [8.0.0, 7.2.1].', - metadata: { - severity: 1000, - cluster_uuid: clusterUuid, + const kibanaVersions = [ + { + versions: ['8.0.0', '7.2.1'], + clusterUuid, + ccs, }, - }; + ]; const replaceState = jest.fn(); const scheduleActions = jest.fn(); @@ -101,8 +102,8 @@ describe('KibanaVersionMismatchAlert', () => { beforeEach(() => { // @ts-ignore Date = FakeDate; - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return [legacyAlert]; + (fetchKibanaVersions as jest.Mock).mockImplementation(() => { + return kibanaVersions; }); (fetchClusters as jest.Mock).mockImplementation(() => { return [{ clusterUuid, clusterName }]; @@ -127,12 +128,19 @@ describe('KibanaVersionMismatchAlert', () => { alertStates: [ { cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, - ccs: undefined, - nodeName: 'Kibana instance alert', + ccs, + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + versions: ['8.0.0', '7.2.1'], + }, ui: { isFiring: true, message: { - text: 'Multiple versions of Kibana ([8.0.0, 7.2.1]) running in this cluster.', + text: 'Multiple versions of Kibana (8.0.0, 7.2.1) running in this cluster.', }, severity: 'warning', triggeredMS: 1, @@ -142,21 +150,26 @@ describe('KibanaVersionMismatchAlert', () => { ], }); expect(scheduleActions).toHaveBeenCalledWith('default', { - action: '[View instances](kibana/instances)', + action: `[View instances](UNIT_TEST_URL/app/monitoring#/kibana/instances?_g=(cluster_uuid:${clusterUuid}))`, actionPlain: 'Verify you have the same version across all instances.', - internalFullMessage: - 'Kibana version mismatch alert is firing for testCluster. Kibana is running [8.0.0, 7.2.1]. [View instances](kibana/instances)', + internalFullMessage: `Kibana version mismatch alert is firing for testCluster. Kibana is running 8.0.0, 7.2.1. [View instances](UNIT_TEST_URL/app/monitoring#/kibana/instances?_g=(cluster_uuid:${clusterUuid}))`, internalShortMessage: 'Kibana version mismatch alert is firing for testCluster. Verify you have the same version across all instances.', - versionList: '[8.0.0, 7.2.1]', + versionList: ['8.0.0', '7.2.1'], clusterName, state: 'firing', }); }); - it('should not fire actions if there is no legacy alert', async () => { - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return []; + it('should not fire actions if there is no mismatch', async () => { + (fetchKibanaVersions as jest.Mock).mockImplementation(() => { + return [ + { + versions: ['8.0.0'], + clusterUuid, + ccs, + }, + ]; }); const alert = new KibanaVersionMismatchAlert(); const type = alert.getAlertType(); diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts index 4fe71e7c27146..f1f8959787003 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts @@ -12,29 +12,29 @@ import { AlertCluster, AlertState, AlertMessage, - LegacyAlert, + AlertInstanceState, + CommonAlertParams, + AlertVersions, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { ALERT_KIBANA_VERSION_MISMATCH, LEGACY_ALERT_DETAILS } from '../../common/constants'; +import { + ALERT_KIBANA_VERSION_MISMATCH, + LEGACY_ALERT_DETAILS, + INDEX_PATTERN_KIBANA, +} from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerts/common'; +import { Globals } from '../static_globals'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { fetchKibanaVersions } from '../lib/alerts/fetch_kibana_versions'; export class KibanaVersionMismatchAlert extends BaseAlert { constructor(public rawAlert?: SanitizedAlert) { super(rawAlert, { id: ALERT_KIBANA_VERSION_MISMATCH, name: LEGACY_ALERT_DETAILS[ALERT_KIBANA_VERSION_MISMATCH].label, - legacy: { - watchName: 'kibana_version_mismatch', - nodeNameLabel: i18n.translate( - 'xpack.monitoring.alerts.kibanaVersionMismatch.nodeNameLabel', - { - defaultMessage: 'Kibana instance alert', - } - ), - changeDataValues: { severity: AlertSeverity.Warning }, - }, interval: '1d', actionVariables: [ { @@ -64,13 +64,40 @@ export class KibanaVersionMismatchAlert extends BaseAlert { }); } + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + availableCcs: string[] + ): Promise { + let kibanaIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_KIBANA); + if (availableCcs) { + kibanaIndexPattern = getCcsIndexPattern(kibanaIndexPattern, availableCcs); + } + const kibanaVersions = await fetchKibanaVersions( + callCluster, + clusters, + kibanaIndexPattern, + Globals.app.config.ui.max_bucket_size + ); + + return kibanaVersions.map((kibanaVersion) => { + return { + shouldFire: kibanaVersion.versions.length > 1, + severity: AlertSeverity.Warning, + meta: kibanaVersion, + clusterUuid: kibanaVersion.clusterUuid, + ccs: kibanaVersion.ccs, + }; + }); + } + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const legacyAlert = item.meta as LegacyAlert; - const versions = this.getVersions(legacyAlert); + const { versions } = item.meta as AlertVersions; const text = i18n.translate('xpack.monitoring.alerts.kibanaVersionMismatch.ui.firingMessage', { defaultMessage: `Multiple versions of Kibana ({versions}) running in this cluster.`, values: { - versions, + versions: versions.join(', '), }, }); @@ -81,54 +108,64 @@ export class KibanaVersionMismatchAlert extends BaseAlert { protected async executeActions( instance: AlertInstance, - alertState: AlertState, - item: AlertData, + { alertStates }: AlertInstanceState, + item: AlertData | null, cluster: AlertCluster ) { - const legacyAlert = item.meta as LegacyAlert; - const versions = this.getVersions(legacyAlert); - if (alertState.ui.isFiring) { - const shortActionText = i18n.translate( - 'xpack.monitoring.alerts.kibanaVersionMismatch.shortAction', - { - defaultMessage: 'Verify you have the same version across all instances.', - } - ); - const fullActionText = i18n.translate( - 'xpack.monitoring.alerts.kibanaVersionMismatch.fullAction', + if (alertStates.length === 0) { + return; + } + + // Logic in the base alert assumes that all alerts will operate against multiple nodes/instances (such as a CPU alert against ES nodes) + // However, some alerts operate on the state of the cluster itself and are only concerned with a single state + const state = alertStates[0]; + const { versions } = state.meta as AlertVersions; + const shortActionText = i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.shortAction', + { + defaultMessage: 'Verify you have the same version across all instances.', + } + ); + const fullActionText = i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.fullAction', + { + defaultMessage: 'View instances', + } + ); + const globalStateLink = this.createGlobalStateLink( + 'kibana/instances', + cluster.clusterUuid, + state.ccs + ); + const action = `[${fullActionText}](${globalStateLink})`; + const internalFullMessage = i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalFullMessage', + { + defaultMessage: `Kibana version mismatch alert is firing for {clusterName}. Kibana is running {versions}. {action}`, + values: { + clusterName: cluster.clusterName, + versions: versions.join(', '), + action, + }, + } + ); + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalShortMessage', { - defaultMessage: 'View instances', + defaultMessage: `Kibana version mismatch alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, } - ); - const action = `[${fullActionText}](kibana/instances)`; - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalShortMessage', - { - defaultMessage: `Kibana version mismatch alert is firing for {clusterName}. {shortActionText}`, - values: { - clusterName: cluster.clusterName, - shortActionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalFullMessage', - { - defaultMessage: `Kibana version mismatch alert is firing for {clusterName}. Kibana is running {versions}. {action}`, - values: { - clusterName: cluster.clusterName, - versions, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - clusterName: cluster.clusterName, - versionList: versions, - action, - actionPlain: shortActionText, - }); - } + ), + internalFullMessage, + state: AlertingDefaults.ALERT_STATE.firing, + clusterName: cluster.clusterName, + versionList: versions, + action, + actionPlain: shortActionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts index fa2740eb9aa1e..0d1c1d20097e5 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts @@ -7,23 +7,20 @@ import { LicenseExpirationAlert } from './license_expiration_alert'; import { ALERT_LICENSE_EXPIRATION } from '../../common/constants'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { AlertSeverity } from '../../common/enums'; +import { fetchLicenses } from '../lib/alerts/fetch_licenses'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; const RealDate = Date; -jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ - fetchLegacyAlerts: jest.fn(), +jest.mock('../lib/alerts/fetch_licenses', () => ({ + fetchLicenses: jest.fn(), })); jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), })); jest.mock('moment', () => { - const moment = function () { - return { - format: () => 'THE_DATE', - }; - }; + const moment = function () {}; moment.duration = () => ({ humanize: () => 'HUMANIZED_DURATION' }); return moment; }); @@ -76,15 +73,11 @@ describe('LicenseExpirationAlert', () => { const clusterUuid = 'abc123'; const clusterName = 'testCluster'; - const legacyAlert = { - prefix: - 'The license for this cluster expires in {{#relativeTime}}metadata.time{{/relativeTime}} at {{#absoluteTime}}metadata.time{{/absoluteTime}}.', - message: 'Update your license.', - metadata: { - severity: 1000, - cluster_uuid: clusterUuid, - time: 1, - }, + const license = { + status: 'expired', + type: 'gold', + expiryDateMS: 1000 * 60 * 60 * 24 * 59, + clusterUuid, }; const replaceState = jest.fn(); @@ -107,8 +100,8 @@ describe('LicenseExpirationAlert', () => { beforeEach(() => { // @ts-ignore Date = FakeDate; - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return [legacyAlert]; + (fetchLicenses as jest.Mock).mockImplementation(() => { + return [license]; }); (fetchClusters as jest.Mock).mockImplementation(() => { return [{ clusterUuid, clusterName }]; @@ -134,7 +127,15 @@ describe('LicenseExpirationAlert', () => { { cluster: { clusterUuid, clusterName }, ccs: undefined, - nodeName: 'Elasticsearch cluster alert', + itemLabel: undefined, + meta: { + clusterUuid: 'abc123', + expiryDateMS: 5097600000, + status: 'expired', + type: 'gold', + }, + nodeId: undefined, + nodeName: undefined, ui: { isFiring: true, message: { @@ -146,14 +147,14 @@ describe('LicenseExpirationAlert', () => { type: 'time', isRelative: true, isAbsolute: false, - timestamp: 1, + timestamp: 5097600000, }, { startToken: '#absolute', type: 'time', isAbsolute: true, isRelative: false, - timestamp: 1, + timestamp: 5097600000, }, { startToken: '#start_link', @@ -163,7 +164,7 @@ describe('LicenseExpirationAlert', () => { }, ], }, - severity: 'warning', + severity: 'danger', triggeredMS: 1, lastCheckedMS: 0, }, @@ -183,9 +184,16 @@ describe('LicenseExpirationAlert', () => { }); }); - it('should not fire actions if there is no legacy alert', async () => { - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return []; + it('should not fire actions if the license is not expired', async () => { + (fetchLicenses as jest.Mock).mockImplementation(() => { + return [ + { + status: 'active', + type: 'gold', + expiryDateMS: 1000 * 60 * 60 * 24 * 61, + clusterUuid, + }, + ]; }); const alert = new LicenseExpirationAlert(); const type = alert.getAlertType(); @@ -197,5 +205,47 @@ describe('LicenseExpirationAlert', () => { expect(replaceState).not.toHaveBeenCalledWith({}); expect(scheduleActions).not.toHaveBeenCalled(); }); + + it('should use danger severity for a license expiring soon', async () => { + (fetchLicenses as jest.Mock).mockImplementation(() => { + return [ + { + status: 'active', + type: 'gold', + expiryDateMS: 1000 * 60 * 60 * 24 * 2, + clusterUuid, + }, + ]; + }); + const alert = new LicenseExpirationAlert(); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.alertOptions.defaultParams, + } as any); + expect(replaceState.mock.calls[0][0].alertStates[0].ui.severity).toBe(AlertSeverity.Danger); + }); + + it('should use warning severity for a license expiring in a bit', async () => { + (fetchLicenses as jest.Mock).mockImplementation(() => { + return [ + { + status: 'active', + type: 'gold', + expiryDateMS: 1000 * 60 * 60 * 24 * 31, + clusterUuid, + }, + ]; + }); + const alert = new LicenseExpirationAlert(); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.alertOptions.defaultParams, + } as any); + expect(replaceState.mock.calls[0][0].alertStates[0].ui.severity).toBe(AlertSeverity.Warning); + }); }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts index cd59fa63f3b2e..24fbd98ef2e8b 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { BaseAlert } from './base_alert'; @@ -15,26 +14,32 @@ import { AlertMessage, AlertMessageTimeToken, AlertMessageLinkToken, - LegacyAlert, + AlertInstanceState, + CommonAlertParams, + AlertLicense, + AlertLicenseState, } from '../../common/types/alerts'; import { AlertExecutorOptions, AlertInstance } from '../../../alerts/server'; -import { ALERT_LICENSE_EXPIRATION, LEGACY_ALERT_DETAILS } from '../../common/constants'; -import { AlertMessageTokenType } from '../../common/enums'; +import { + ALERT_LICENSE_EXPIRATION, + LEGACY_ALERT_DETAILS, + INDEX_PATTERN_ELASTICSEARCH, +} from '../../common/constants'; +import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerts/common'; import { Globals } from '../static_globals'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { fetchLicenses } from '../lib/alerts/fetch_licenses'; + +const EXPIRES_DAYS = [60, 30, 14, 7]; export class LicenseExpirationAlert extends BaseAlert { constructor(public rawAlert?: SanitizedAlert) { super(rawAlert, { id: ALERT_LICENSE_EXPIRATION, name: LEGACY_ALERT_DETAILS[ALERT_LICENSE_EXPIRATION].label, - legacy: { - watchName: 'xpack_license_expiration', - nodeNameLabel: i18n.translate('xpack.monitoring.alerts.licenseExpiration.nodeNameLabel', { - defaultMessage: 'Elasticsearch cluster alert', - }), - }, interval: '1d', actionVariables: [ { @@ -71,8 +76,53 @@ export class LicenseExpirationAlert extends BaseAlert { return await super.execute(options); } + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + availableCcs: string[] + ): Promise { + let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); + if (availableCcs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + const licenses = await fetchLicenses(callCluster, clusters, esIndexPattern); + + return licenses.map((license) => { + const { clusterUuid, type, expiryDateMS, status, ccs } = license; + let isExpired = false; + let severity = AlertSeverity.Success; + + if (status !== 'active') { + isExpired = true; + severity = AlertSeverity.Danger; + } else if (expiryDateMS) { + for (let i = EXPIRES_DAYS.length - 1; i >= 0; i--) { + if (type === 'trial' && i < 2) { + break; + } + + const fromNow = +new Date() + EXPIRES_DAYS[i] * 1000 * 60 * 60 * 24; + if (fromNow >= expiryDateMS) { + isExpired = true; + severity = i < 1 ? AlertSeverity.Warning : AlertSeverity.Danger; + break; + } + } + } + + return { + shouldFire: isExpired, + severity, + meta: license, + clusterUuid, + ccs, + }; + }); + } + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const legacyAlert = item.meta as LegacyAlert; + const license = item.meta as AlertLicense; return { text: i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.firingMessage', { defaultMessage: `The license for this cluster expires in #relative at #absolute. #start_linkPlease update your license.#end_link`, @@ -83,14 +133,14 @@ export class LicenseExpirationAlert extends BaseAlert { type: AlertMessageTokenType.Time, isRelative: true, isAbsolute: false, - timestamp: legacyAlert.metadata.time, + timestamp: license.expiryDateMS, } as AlertMessageTimeToken, { startToken: '#absolute', type: AlertMessageTokenType.Time, isAbsolute: true, isRelative: false, - timestamp: legacyAlert.metadata.time, + timestamp: license.expiryDateMS, } as AlertMessageTimeToken, { startToken: '#start_link', @@ -104,48 +154,51 @@ export class LicenseExpirationAlert extends BaseAlert { protected async executeActions( instance: AlertInstance, - alertState: AlertState, - item: AlertData, + { alertStates }: AlertInstanceState, + item: AlertData | null, cluster: AlertCluster ) { - const legacyAlert = item.meta as LegacyAlert; - const $expiry = moment(legacyAlert.metadata.time); - const $duration = moment.duration(+new Date() - $expiry.valueOf()); - if (alertState.ui.isFiring) { - const actionText = i18n.translate('xpack.monitoring.alerts.licenseExpiration.action', { - defaultMessage: 'Please update your license.', - }); - const action = `[${actionText}](elasticsearch/nodes)`; - const expiredDate = $duration.humanize(); - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.licenseExpiration.firing.internalShortMessage', - { - defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {actionText}`, - values: { - clusterName: cluster.clusterName, - expiredDate, - actionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.licenseExpiration.firing.internalFullMessage', - { - defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {action}`, - values: { - clusterName: cluster.clusterName, - expiredDate, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - expiredDate, - clusterName: cluster.clusterName, - action, - actionPlain: actionText, - }); + if (alertStates.length === 0) { + return; } + + // Logic in the base alert assumes that all alerts will operate against multiple nodes/instances (such as a CPU alert against ES nodes) + // However, some alerts operate on the state of the cluster itself and are only concerned with a single state + const state: AlertLicenseState = alertStates[0] as AlertLicenseState; + const $duration = moment.duration(+new Date() - state.expiryDateMS); + const actionText = i18n.translate('xpack.monitoring.alerts.licenseExpiration.action', { + defaultMessage: 'Please update your license.', + }); + const action = `[${actionText}](elasticsearch/nodes)`; + const expiredDate = $duration.humanize(); + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.firing.internalShortMessage', + { + defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {actionText}`, + values: { + clusterName: cluster.clusterName, + expiredDate, + actionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.firing.internalFullMessage', + { + defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {action}`, + values: { + clusterName: cluster.clusterName, + expiredDate, + action, + }, + } + ), + state: AlertingDefaults.ALERT_STATE.firing, + expiredDate, + clusterName: cluster.clusterName, + action, + actionPlain: actionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts index 514fd71368085..50a826b36d58f 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts @@ -7,13 +7,13 @@ import { LogstashVersionMismatchAlert } from './logstash_version_mismatch_alert'; import { ALERT_LOGSTASH_VERSION_MISMATCH } from '../../common/constants'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchLogstashVersions } from '../lib/alerts/fetch_logstash_versions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; const RealDate = Date; -jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ - fetchLegacyAlerts: jest.fn(), +jest.mock('../lib/alerts/fetch_logstash_versions', () => ({ + fetchLogstashVersions: jest.fn(), })); jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), @@ -22,6 +22,7 @@ jest.mock('../lib/alerts/fetch_clusters', () => ({ jest.mock('../static_globals', () => ({ Globals: { app: { + url: 'UNIT_TEST_URL', getLogger: () => ({ debug: jest.fn() }), config: { ui: { @@ -68,16 +69,16 @@ describe('LogstashVersionMismatchAlert', () => { function FakeDate() {} FakeDate.prototype.valueOf = () => 1; + const ccs = undefined; const clusterUuid = 'abc123'; const clusterName = 'testCluster'; - const legacyAlert = { - prefix: 'This cluster is running with multiple versions of Logstash.', - message: 'Versions: [8.0.0, 7.2.1].', - metadata: { - severity: 1000, - cluster_uuid: clusterUuid, + const logstashVersions = [ + { + versions: ['8.0.0', '7.2.1'], + clusterUuid, + ccs, }, - }; + ]; const replaceState = jest.fn(); const scheduleActions = jest.fn(); @@ -99,8 +100,8 @@ describe('LogstashVersionMismatchAlert', () => { beforeEach(() => { // @ts-ignore Date = FakeDate; - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return [legacyAlert]; + (fetchLogstashVersions as jest.Mock).mockImplementation(() => { + return logstashVersions; }); (fetchClusters as jest.Mock).mockImplementation(() => { return [{ clusterUuid, clusterName }]; @@ -126,12 +127,19 @@ describe('LogstashVersionMismatchAlert', () => { alertStates: [ { cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, - ccs: undefined, - nodeName: 'Logstash node alert', + ccs, + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + versions: ['8.0.0', '7.2.1'], + }, ui: { isFiring: true, message: { - text: 'Multiple versions of Logstash ([8.0.0, 7.2.1]) running in this cluster.', + text: 'Multiple versions of Logstash (8.0.0, 7.2.1) running in this cluster.', }, severity: 'warning', triggeredMS: 1, @@ -141,21 +149,26 @@ describe('LogstashVersionMismatchAlert', () => { ], }); expect(scheduleActions).toHaveBeenCalledWith('default', { - action: '[View nodes](logstash/nodes)', + action: `[View nodes](UNIT_TEST_URL/app/monitoring#/logstash/nodes?_g=(cluster_uuid:${clusterUuid}))`, actionPlain: 'Verify you have the same version across all nodes.', - internalFullMessage: - 'Logstash version mismatch alert is firing for testCluster. Logstash is running [8.0.0, 7.2.1]. [View nodes](logstash/nodes)', + internalFullMessage: `Logstash version mismatch alert is firing for testCluster. Logstash is running 8.0.0, 7.2.1. [View nodes](UNIT_TEST_URL/app/monitoring#/logstash/nodes?_g=(cluster_uuid:${clusterUuid}))`, internalShortMessage: 'Logstash version mismatch alert is firing for testCluster. Verify you have the same version across all nodes.', - versionList: '[8.0.0, 7.2.1]', + versionList: ['8.0.0', '7.2.1'], clusterName, state: 'firing', }); }); - it('should not fire actions if there is no legacy alert', async () => { - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return []; + it('should not fire actions if there is no mismatch', async () => { + (fetchLogstashVersions as jest.Mock).mockImplementation(() => { + return [ + { + versions: ['8.0.0'], + clusterUuid, + ccs, + }, + ]; }); const alert = new LogstashVersionMismatchAlert(); const type = alert.getAlertType(); diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts index 0dc93743e2276..d903dd49600ad 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts @@ -12,29 +12,29 @@ import { AlertCluster, AlertState, AlertMessage, - LegacyAlert, + AlertInstanceState, + CommonAlertParams, + AlertVersions, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { ALERT_LOGSTASH_VERSION_MISMATCH, LEGACY_ALERT_DETAILS } from '../../common/constants'; +import { + ALERT_LOGSTASH_VERSION_MISMATCH, + LEGACY_ALERT_DETAILS, + INDEX_PATTERN_LOGSTASH, +} from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerts/common'; +import { Globals } from '../static_globals'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { fetchLogstashVersions } from '../lib/alerts/fetch_logstash_versions'; export class LogstashVersionMismatchAlert extends BaseAlert { constructor(public rawAlert?: SanitizedAlert) { super(rawAlert, { id: ALERT_LOGSTASH_VERSION_MISMATCH, name: LEGACY_ALERT_DETAILS[ALERT_LOGSTASH_VERSION_MISMATCH].label, - legacy: { - watchName: 'logstash_version_mismatch', - nodeNameLabel: i18n.translate( - 'xpack.monitoring.alerts.logstashVersionMismatch.nodeNameLabel', - { - defaultMessage: 'Logstash node alert', - } - ), - changeDataValues: { severity: AlertSeverity.Warning }, - }, interval: '1d', actionVariables: [ { @@ -51,15 +51,42 @@ export class LogstashVersionMismatchAlert extends BaseAlert { }); } + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + availableCcs: string[] + ): Promise { + let logstashIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_LOGSTASH); + if (availableCcs) { + logstashIndexPattern = getCcsIndexPattern(logstashIndexPattern, availableCcs); + } + const logstashVersions = await fetchLogstashVersions( + callCluster, + clusters, + logstashIndexPattern, + Globals.app.config.ui.max_bucket_size + ); + + return logstashVersions.map((logstashVersion) => { + return { + shouldFire: logstashVersion.versions.length > 1, + severity: AlertSeverity.Warning, + meta: logstashVersion, + clusterUuid: logstashVersion.clusterUuid, + ccs: logstashVersion.ccs, + }; + }); + } + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const legacyAlert = item.meta as LegacyAlert; - const versions = this.getVersions(legacyAlert); + const { versions } = item.meta as AlertVersions; const text = i18n.translate( 'xpack.monitoring.alerts.logstashVersionMismatch.ui.firingMessage', { defaultMessage: `Multiple versions of Logstash ({versions}) running in this cluster.`, values: { - versions, + versions: versions.join(', '), }, } ); @@ -71,54 +98,63 @@ export class LogstashVersionMismatchAlert extends BaseAlert { protected async executeActions( instance: AlertInstance, - alertState: AlertState, - item: AlertData, + { alertStates }: AlertInstanceState, + item: AlertData | null, cluster: AlertCluster ) { - const legacyAlert = item.meta as LegacyAlert; - const versions = this.getVersions(legacyAlert); - if (alertState.ui.isFiring) { - const shortActionText = i18n.translate( - 'xpack.monitoring.alerts.logstashVersionMismatch.shortAction', + if (alertStates.length === 0) { + return; + } + + // Logic in the base alert assumes that all alerts will operate against multiple nodes/instances (such as a CPU alert against ES nodes) + // However, some alerts operate on the state of the cluster itself and are only concerned with a single state + const state = alertStates[0]; + const { versions } = state.meta as AlertVersions; + const shortActionText = i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.shortAction', + { + defaultMessage: 'Verify you have the same version across all nodes.', + } + ); + const fullActionText = i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.fullAction', + { + defaultMessage: 'View nodes', + } + ); + const globalStateLink = this.createGlobalStateLink( + 'logstash/nodes', + cluster.clusterUuid, + state.ccs + ); + const action = `[${fullActionText}](${globalStateLink})`; + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalShortMessage', { - defaultMessage: 'Verify you have the same version across all nodes.', + defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, } - ); - const fullActionText = i18n.translate( - 'xpack.monitoring.alerts.logstashVersionMismatch.fullAction', + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalFullMessage', { - defaultMessage: 'View nodes', + defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. Logstash is running {versions}. {action}`, + values: { + clusterName: cluster.clusterName, + versions: versions.join(', '), + action, + }, } - ); - const action = `[${fullActionText}](logstash/nodes)`; - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalShortMessage', - { - defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. {shortActionText}`, - values: { - clusterName: cluster.clusterName, - shortActionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalFullMessage', - { - defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. Logstash is running {versions}. {action}`, - values: { - clusterName: cluster.clusterName, - versions, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - clusterName: cluster.clusterName, - versionList: versions, - action, - actionPlain: shortActionText, - }); - } + ), + state: AlertingDefaults.ALERT_STATE.firing, + clusterName: cluster.clusterName, + versionList: versions, + action, + actionPlain: shortActionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts index 59b61645e2eca..848436573fab9 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts @@ -7,13 +7,13 @@ import { NodesChangedAlert } from './nodes_changed_alert'; import { ALERT_NODES_CHANGED } from '../../common/constants'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchNodesFromClusterStats } from '../lib/alerts/fetch_nodes_from_cluster_stats'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; const RealDate = Date; -jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ - fetchLegacyAlerts: jest.fn(), +jest.mock('../lib/alerts/fetch_nodes_from_cluster_stats', () => ({ + fetchNodesFromClusterStats: jest.fn(), })); jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), @@ -73,23 +73,33 @@ describe('NodesChangedAlert', () => { function FakeDate() {} FakeDate.prototype.valueOf = () => 1; + const nodeUuid = 'myNodeUuid'; + const nodeEphemeralId = 'myEphemeralId'; + const nodeEphemeralIdChanged = 'myEphemeralIdChanged'; + const nodeName = 'test'; + const ccs = undefined; const clusterUuid = 'abc123'; const clusterName = 'testCluster'; - const legacyAlert = { - prefix: 'Elasticsearch cluster nodes have changed!', - message: 'Node was restarted [1]: [test].', - metadata: { - severity: 1000, - cluster_uuid: clusterUuid, - }, - nodes: { - added: {}, - removed: {}, - restarted: { - test: 'test', - }, + const nodes = [ + { + recentNodes: [ + { + nodeUuid, + nodeEphemeralId: nodeEphemeralIdChanged, + nodeName, + }, + ], + priorNodes: [ + { + nodeUuid, + nodeEphemeralId, + nodeName, + }, + ], + clusterUuid, + ccs, }, - }; + ]; const replaceState = jest.fn(); const scheduleActions = jest.fn(); @@ -111,8 +121,8 @@ describe('NodesChangedAlert', () => { beforeEach(() => { // @ts-ignore Date = FakeDate; - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return [legacyAlert]; + (fetchNodesFromClusterStats as jest.Mock).mockImplementation(() => { + return nodes; }); (fetchClusters as jest.Mock).mockImplementation(() => { return [{ clusterUuid, clusterName }]; @@ -138,8 +148,28 @@ describe('NodesChangedAlert', () => { alertStates: [ { cluster: { clusterUuid, clusterName }, - ccs: undefined, - nodeName: 'Elasticsearch nodes alert', + ccs, + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + recentNodes: [ + { + nodeUuid, + nodeEphemeralId: nodeEphemeralIdChanged, + nodeName, + }, + ], + priorNodes: [ + { + nodeUuid, + nodeEphemeralId, + nodeName, + }, + ], + }, ui: { isFiring: true, message: { @@ -167,9 +197,28 @@ describe('NodesChangedAlert', () => { }); }); - it('should not fire actions if there is no legacy alert', async () => { - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return []; + it('should not fire actions if no nodes have changed', async () => { + (fetchNodesFromClusterStats as jest.Mock).mockImplementation(() => { + return [ + { + recentNodes: [ + { + nodeUuid, + nodeEphemeralId, + nodeName, + }, + ], + priorNodes: [ + { + nodeUuid, + nodeEphemeralId, + nodeName, + }, + ], + clusterUuid, + ccs, + }, + ]; }); const alert = new NodesChangedAlert(); const type = alert.getAlertType(); diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts index 10dc6f911409e..63b3ef672405e 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts @@ -12,26 +12,61 @@ import { AlertCluster, AlertState, AlertMessage, - LegacyAlert, - LegacyAlertNodesChangedList, + AlertClusterStatsNodes, + AlertClusterStatsNode, + CommonAlertParams, + AlertInstanceState, + AlertNodesChangedState, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { ALERT_NODES_CHANGED, LEGACY_ALERT_DETAILS } from '../../common/constants'; +import { + ALERT_NODES_CHANGED, + LEGACY_ALERT_DETAILS, + INDEX_PATTERN_ELASTICSEARCH, +} from '../../common/constants'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerts/common'; +import { Globals } from '../static_globals'; +import { fetchNodesFromClusterStats } from '../lib/alerts/fetch_nodes_from_cluster_stats'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { AlertSeverity } from '../../common/enums'; + +interface AlertNodesChangedStates { + removed: AlertClusterStatsNode[]; + added: AlertClusterStatsNode[]; + restarted: AlertClusterStatsNode[]; +} + +function getNodeStates(nodes: AlertClusterStatsNodes): AlertNodesChangedStates { + const removed = nodes.priorNodes.filter( + (priorNode) => + !nodes.recentNodes.find((recentNode) => priorNode.nodeUuid === recentNode.nodeUuid) + ); + const added = nodes.recentNodes.filter( + (recentNode) => + !nodes.priorNodes.find((priorNode) => priorNode.nodeUuid === recentNode.nodeUuid) + ); + const restarted = nodes.recentNodes.filter( + (recentNode) => + nodes.priorNodes.find((priorNode) => priorNode.nodeUuid === recentNode.nodeUuid) && + !nodes.priorNodes.find( + (priorNode) => priorNode.nodeEphemeralId === recentNode.nodeEphemeralId + ) + ); + + return { + removed, + added, + restarted, + }; +} export class NodesChangedAlert extends BaseAlert { constructor(public rawAlert?: SanitizedAlert) { super(rawAlert, { id: ALERT_NODES_CHANGED, name: LEGACY_ALERT_DETAILS[ALERT_NODES_CHANGED].label, - legacy: { - watchName: 'elasticsearch_nodes', - nodeNameLabel: i18n.translate('xpack.monitoring.alerts.nodesChanged.nodeNameLabel', { - defaultMessage: 'Elasticsearch nodes alert', - }), - changeDataValues: { shouldFire: true }, - }, actionVariables: [ { name: 'added', @@ -65,13 +100,39 @@ export class NodesChangedAlert extends BaseAlert { }); } - private getNodeStates(legacyAlert: LegacyAlert): LegacyAlertNodesChangedList { - return legacyAlert.nodes || { added: {}, removed: {}, restarted: {} }; + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + availableCcs: string[] + ): Promise { + let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); + if (availableCcs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + const nodesFromClusterStats = await fetchNodesFromClusterStats( + callCluster, + clusters, + esIndexPattern + ); + return nodesFromClusterStats.map((nodes) => { + const { removed, added, restarted } = getNodeStates(nodes); + const shouldFire = removed.length > 0 || added.length > 0 || restarted.length > 0; + const severity = AlertSeverity.Warning; + + return { + shouldFire, + severity, + meta: nodes, + clusterUuid: nodes.clusterUuid, + ccs: nodes.ccs, + }; + }); } protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const legacyAlert = item.meta as LegacyAlert; - const states = this.getNodeStates(legacyAlert); + const nodes = item.meta as AlertClusterStatsNodes; + const states = getNodeStates(nodes); if (!alertState.ui.isFiring) { return { text: i18n.translate('xpack.monitoring.alerts.nodesChanged.ui.resolvedMessage', { @@ -80,11 +141,7 @@ export class NodesChangedAlert extends BaseAlert { }; } - if ( - Object.values(states.added).length === 0 && - Object.values(states.removed).length === 0 && - Object.values(states.restarted).length === 0 - ) { + if (states.added.length === 0 && states.removed.length === 0 && states.restarted.length === 0) { return { text: i18n.translate( 'xpack.monitoring.alerts.nodesChanged.ui.nothingDetectedFiringMessage', @@ -96,29 +153,29 @@ export class NodesChangedAlert extends BaseAlert { } const addedText = - Object.values(states.added).length > 0 + states.added.length > 0 ? i18n.translate('xpack.monitoring.alerts.nodesChanged.ui.addedFiringMessage', { defaultMessage: `Elasticsearch nodes '{added}' added to this cluster.`, values: { - added: Object.values(states.added).join(','), + added: states.added.map((n) => n.nodeName).join(','), }, }) : null; const removedText = - Object.values(states.removed).length > 0 + states.removed.length > 0 ? i18n.translate('xpack.monitoring.alerts.nodesChanged.ui.removedFiringMessage', { defaultMessage: `Elasticsearch nodes '{removed}' removed from this cluster.`, values: { - removed: Object.values(states.removed).join(','), + removed: states.removed.map((n) => n.nodeName).join(','), }, }) : null; const restartedText = - Object.values(states.restarted).length > 0 + states.restarted.length > 0 ? i18n.translate('xpack.monitoring.alerts.nodesChanged.ui.restartedFiringMessage', { defaultMessage: `Elasticsearch nodes '{restarted}' restarted in this cluster.`, values: { - restarted: Object.values(states.restarted).join(','), + restarted: states.restarted.map((n) => n.nodeName).join(','), }, }) : null; @@ -130,55 +187,60 @@ export class NodesChangedAlert extends BaseAlert { protected async executeActions( instance: AlertInstance, - alertState: AlertState, - item: AlertData, + { alertStates }: AlertInstanceState, + item: AlertData | null, cluster: AlertCluster ) { - const legacyAlert = item.meta as LegacyAlert; - if (alertState.ui.isFiring) { - const shortActionText = i18n.translate('xpack.monitoring.alerts.nodesChanged.shortAction', { - defaultMessage: 'Verify that you added, removed, or restarted nodes.', - }); - const fullActionText = i18n.translate('xpack.monitoring.alerts.nodesChanged.fullAction', { - defaultMessage: 'View nodes', - }); - const action = `[${fullActionText}](elasticsearch/nodes)`; - const states = this.getNodeStates(legacyAlert); - const added = Object.values(states.added).join(','); - const removed = Object.values(states.removed).join(','); - const restarted = Object.values(states.restarted).join(','); - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.nodesChanged.firing.internalShortMessage', - { - defaultMessage: `Nodes changed alert is firing for {clusterName}. {shortActionText}`, - values: { - clusterName: cluster.clusterName, - shortActionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.nodesChanged.firing.internalFullMessage', - { - defaultMessage: `Nodes changed alert is firing for {clusterName}. The following Elasticsearch nodes have been added:{added} removed:{removed} restarted:{restarted}. {action}`, - values: { - clusterName: cluster.clusterName, - added, - removed, - restarted, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - clusterName: cluster.clusterName, - added, - removed, - restarted, - action, - actionPlain: shortActionText, - }); + if (alertStates.length === 0) { + return; } + + // Logic in the base alert assumes that all alerts will operate against multiple nodes/instances (such as a CPU alert against ES nodes) + // However, some alerts operate on the state of the cluster itself and are only concerned with a single state + const state = alertStates[0] as AlertNodesChangedState; + const nodes = state.meta as AlertClusterStatsNodes; + const shortActionText = i18n.translate('xpack.monitoring.alerts.nodesChanged.shortAction', { + defaultMessage: 'Verify that you added, removed, or restarted nodes.', + }); + const fullActionText = i18n.translate('xpack.monitoring.alerts.nodesChanged.fullAction', { + defaultMessage: 'View nodes', + }); + const action = `[${fullActionText}](elasticsearch/nodes)`; + const states = getNodeStates(nodes); + const added = states.added.map((node) => node.nodeName).join(','); + const removed = states.removed.map((node) => node.nodeName).join(','); + const restarted = states.restarted.map((node) => node.nodeName).join(','); + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.firing.internalShortMessage', + { + defaultMessage: `Nodes changed alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.firing.internalFullMessage', + { + defaultMessage: `Nodes changed alert is firing for {clusterName}. The following Elasticsearch nodes have been added:{added} removed:{removed} restarted:{restarted}. {action}`, + values: { + clusterName: cluster.clusterName, + added, + removed, + restarted, + action, + }, + } + ), + state: AlertingDefaults.ALERT_STATE.firing, + clusterName: cluster.clusterName, + added, + removed, + restarted, + action, + actionPlain: shortActionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts new file mode 100644 index 0000000000000..2fdbbe80b7e89 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts @@ -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 { fetchClusterHealth } from './fetch_cluster_health'; + +describe('fetchClusterHealth', () => { + it('should return the cluster health', async () => { + const status = 'green'; + const clusterUuid = 'sdfdsaj34434'; + const callCluster = jest.fn(() => ({ + hits: { + hits: [ + { + _index: '.monitoring-es-7', + _source: { + cluster_state: { + status, + }, + cluster_uuid: clusterUuid, + }, + }, + ], + }, + })); + + const clusters = [{ clusterUuid, clusterName: 'foo' }]; + const index = '.monitoring-es-*'; + + const health = await fetchClusterHealth(callCluster, clusters, index); + expect(health).toEqual([ + { + health: status, + clusterUuid, + ccs: undefined, + }, + ]); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts new file mode 100644 index 0000000000000..bcfa2da0958a2 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts @@ -0,0 +1,69 @@ +/* + * 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 { AlertCluster, AlertClusterHealth } from '../../../common/types/alerts'; +import { ElasticsearchSource } from '../../../common/types/es'; + +export async function fetchClusterHealth( + callCluster: any, + clusters: AlertCluster[], + index: string +): Promise { + const params = { + index, + filterPath: [ + 'hits.hits._source.cluster_state.status', + 'hits.hits._source.cluster_uuid', + 'hits.hits._index', + ], + body: { + size: clusters.length, + sort: [ + { + timestamp: { + order: 'desc', + unmapped_type: 'long', + }, + }, + ], + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), + }, + }, + { + term: { + type: 'cluster_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + collapse: { + field: 'cluster_uuid', + }, + }, + }; + + const response = await callCluster('search', params); + return response.hits.hits.map((hit: { _source: ElasticsearchSource; _index: string }) => { + return { + health: hit._source.cluster_state?.status, + clusterUuid: hit._source.cluster_uuid, + ccs: hit._index.includes(':') ? hit._index.split(':')[0] : undefined, + } as AlertClusterHealth; + }); +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts new file mode 100644 index 0000000000000..e4f4a4d364ebf --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts @@ -0,0 +1,52 @@ +/* + * 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 { fetchElasticsearchVersions } from './fetch_elasticsearch_versions'; + +describe('fetchElasticsearchVersions', () => { + let callCluster = jest.fn(); + const clusters = [ + { + clusterUuid: 'cluster123', + clusterName: 'test-cluster', + }, + ]; + const index = '.monitoring-es-*'; + const size = 10; + const versions = ['8.0.0', '7.2.1']; + + it('fetch as expected', async () => { + callCluster = jest.fn().mockImplementation(() => { + return { + hits: { + hits: [ + { + _index: `Monitoring:${index}`, + _source: { + cluster_uuid: 'cluster123', + cluster_stats: { + nodes: { + versions, + }, + }, + }, + }, + ], + }, + }; + }); + + const result = await fetchElasticsearchVersions(callCluster, clusters, index, size); + expect(result).toEqual([ + { + clusterUuid: clusters[0].clusterUuid, + ccs: 'Monitoring', + versions, + }, + ]); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts new file mode 100644 index 0000000000000..373ddb62aaee8 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts @@ -0,0 +1,71 @@ +/* + * 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 { AlertCluster, AlertVersions } from '../../../common/types/alerts'; +import { ElasticsearchSource } from '../../../common/types/es'; + +export async function fetchElasticsearchVersions( + callCluster: any, + clusters: AlertCluster[], + index: string, + size: number +): Promise { + const params = { + index, + filterPath: [ + 'hits.hits._source.cluster_stats.nodes.versions', + 'hits.hits._index', + 'hits.hits._source.cluster_uuid', + ], + body: { + size: clusters.length, + sort: [ + { + timestamp: { + order: 'desc', + unmapped_type: 'long', + }, + }, + ], + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), + }, + }, + { + term: { + type: 'cluster_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + collapse: { + field: 'cluster_uuid', + }, + }, + }; + + const response = await callCluster('search', params); + return response.hits.hits.map((hit: { _source: ElasticsearchSource; _index: string }) => { + const versions = hit._source.cluster_stats?.nodes?.versions; + return { + versions, + clusterUuid: hit._source.cluster_uuid, + ccs: hit._index.includes(':') ? hit._index.split(':')[0] : null, + }; + }); +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts new file mode 100644 index 0000000000000..518828ef0b1c8 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts @@ -0,0 +1,74 @@ +/* + * 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 { fetchKibanaVersions } from './fetch_kibana_versions'; + +describe('fetchKibanaVersions', () => { + let callCluster = jest.fn(); + const clusters = [ + { + clusterUuid: 'cluster123', + clusterName: 'test-cluster', + }, + ]; + const index = '.monitoring-kibana-*'; + const size = 10; + + it('fetch as expected', async () => { + callCluster = jest.fn().mockImplementation(() => { + return { + aggregations: { + index: { + buckets: [ + { + key: `Monitoring:${index}`, + }, + ], + }, + cluster: { + buckets: [ + { + key: 'cluster123', + group_by_kibana: { + buckets: [ + { + group_by_version: { + buckets: [ + { + key: '8.0.0', + }, + ], + }, + }, + { + group_by_version: { + buckets: [ + { + key: '7.2.1', + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + }; + }); + + const result = await fetchKibanaVersions(callCluster, clusters, index, size); + expect(result).toEqual([ + { + clusterUuid: clusters[0].clusterUuid, + ccs: 'Monitoring', + versions: ['8.0.0', '7.2.1'], + }, + ]); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts new file mode 100644 index 0000000000000..2e7fe192df656 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts @@ -0,0 +1,111 @@ +/* + * 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 { get } from 'lodash'; +import { AlertCluster, AlertVersions } from '../../../common/types/alerts'; + +interface ESAggResponse { + key: string; +} + +export async function fetchKibanaVersions( + callCluster: any, + clusters: AlertCluster[], + index: string, + size: number +): Promise { + const params = { + index, + filterPath: ['aggregations'], + body: { + size: 0, + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), + }, + }, + { + term: { + type: 'kibana_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + aggs: { + index: { + terms: { + field: '_index', + size: 1, + }, + }, + cluster: { + terms: { + field: 'cluster_uuid', + size: 1, + }, + aggs: { + group_by_kibana: { + terms: { + field: 'kibana_stats.kibana.uuid', + size, + }, + aggs: { + group_by_version: { + terms: { + field: 'kibana_stats.kibana.version', + size: 1, + order: { + latest_report: 'desc', + }, + }, + aggs: { + latest_report: { + max: { + field: 'timestamp', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const response = await callCluster('search', params); + const indexName = get(response, 'aggregations.index.buckets[0].key', ''); + const clusterList = get(response, 'aggregations.cluster.buckets', []) as ESAggResponse[]; + return clusterList.map((cluster) => { + const clusterUuid = cluster.key; + const uuids = get(cluster, 'group_by_kibana.buckets', []); + const byVersion: { [version: string]: boolean } = {}; + for (const uuid of uuids) { + const version = get(uuid, 'group_by_version.buckets[0].key', ''); + if (!version) { + continue; + } + byVersion[version] = true; + } + return { + versions: Object.keys(byVersion), + clusterUuid, + ccs: indexName.includes(':') ? indexName.split(':')[0] : null, + }; + }); +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts deleted file mode 100644 index 086c5c7da9139..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts +++ /dev/null @@ -1,96 +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 { fetchLegacyAlerts } from './fetch_legacy_alerts'; - -describe('fetchLegacyAlerts', () => { - let callCluster = jest.fn(); - const clusters = [ - { - clusterUuid: 'abc123', - clusterName: 'test', - }, - ]; - const index = '.monitoring-es-*'; - const size = 10; - - it('fetch legacy alerts', async () => { - const prefix = 'thePrefix'; - const message = 'theMessage'; - const nodes = {}; - const metadata = { - severity: 2000, - cluster_uuid: clusters[0].clusterUuid, - metadata: {}, - }; - callCluster = jest.fn().mockImplementation(() => { - return { - hits: { - hits: [ - { - _source: { - prefix, - message, - nodes, - metadata, - }, - }, - ], - }, - }; - }); - const result = await fetchLegacyAlerts(callCluster, clusters, index, 'myWatch', size); - expect(result).toEqual([ - { - message, - metadata, - nodes, - nodeName: '', - prefix, - }, - ]); - }); - - it('should use consistent params', async () => { - let params = null; - callCluster = jest.fn().mockImplementation((...args) => { - params = args[1]; - }); - await fetchLegacyAlerts(callCluster, clusters, index, 'myWatch', size); - expect(params).toStrictEqual({ - index, - filterPath: [ - 'hits.hits._source.prefix', - 'hits.hits._source.message', - 'hits.hits._source.resolved_timestamp', - 'hits.hits._source.nodes', - 'hits.hits._source.metadata.*', - ], - body: { - size, - sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], - query: { - bool: { - minimum_should_match: 1, - filter: [ - { - terms: { 'metadata.cluster_uuid': clusters.map((cluster) => cluster.clusterUuid) }, - }, - { term: { 'metadata.watch': 'myWatch' } }, - ], - should: [ - { range: { timestamp: { gte: 'now-2m' } } }, - { range: { resolved_timestamp: { gte: 'now-2m' } } }, - { bool: { must_not: { exists: { field: 'resolved_timestamp' } } } }, - ], - }, - }, - collapse: { field: 'metadata.cluster_uuid' }, - }, - }); - }); -}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts deleted file mode 100644 index 96438da111b6d..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts +++ /dev/null @@ -1,97 +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 { get } from 'lodash'; -import { LegacyAlert, AlertCluster, LegacyAlertMetadata } from '../../../common/types/alerts'; - -export async function fetchLegacyAlerts( - callCluster: any, - clusters: AlertCluster[], - index: string, - watchName: string, - size: number -): Promise { - const params = { - index, - filterPath: [ - 'hits.hits._source.prefix', - 'hits.hits._source.message', - 'hits.hits._source.resolved_timestamp', - 'hits.hits._source.nodes', - 'hits.hits._source.metadata.*', - ], - body: { - size, - sort: [ - { - timestamp: { - order: 'desc', - unmapped_type: 'long', - }, - }, - ], - query: { - bool: { - minimum_should_match: 1, - filter: [ - { - terms: { - 'metadata.cluster_uuid': clusters.map((cluster) => cluster.clusterUuid), - }, - }, - { - term: { - 'metadata.watch': watchName, - }, - }, - ], - should: [ - { - range: { - timestamp: { - gte: 'now-2m', - }, - }, - }, - { - range: { - resolved_timestamp: { - gte: 'now-2m', - }, - }, - }, - { - bool: { - must_not: { - exists: { - field: 'resolved_timestamp', - }, - }, - }, - }, - ], - }, - }, - collapse: { - field: 'metadata.cluster_uuid', - }, - }, - }; - - const response = await callCluster('search', params); - return get(response, 'hits.hits', []).map((hit: any) => { - const legacyAlert: LegacyAlert = { - prefix: get(hit, '_source.prefix'), - message: get(hit, '_source.message'), - resolved_timestamp: get(hit, '_source.resolved_timestamp'), - nodes: get(hit, '_source.nodes'), - nodeName: '', // This is set by BaseAlert - metadata: get(hit, '_source.metadata') as LegacyAlertMetadata, - }; - return legacyAlert; - }); -} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts new file mode 100644 index 0000000000000..715c8c50a45e7 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { fetchLicenses } from './fetch_licenses'; + +describe('fetchLicenses', () => { + const clusterName = 'MyCluster'; + const clusterUuid = 'clusterA'; + const license = { + status: 'active', + expiry_date_in_millis: 1579532493876, + type: 'basic', + }; + + it('return a list of licenses', async () => { + const callCluster = jest.fn().mockImplementation(() => ({ + hits: { + hits: [ + { + _source: { + license, + cluster_uuid: clusterUuid, + }, + }, + ], + }, + })); + const clusters = [{ clusterUuid, clusterName }]; + const index = '.monitoring-es-*'; + const result = await fetchLicenses(callCluster, clusters, index); + expect(result).toEqual([ + { + status: license.status, + type: license.type, + expiryDateMS: license.expiry_date_in_millis, + clusterUuid, + }, + ]); + }); + + it('should only search for the clusters provided', async () => { + const callCluster = jest.fn(); + const clusters = [{ clusterUuid, clusterName }]; + const index = '.monitoring-es-*'; + await fetchLicenses(callCluster, clusters, index); + const params = callCluster.mock.calls[0][1]; + expect(params.body.query.bool.filter[0].terms.cluster_uuid).toEqual([clusterUuid]); + }); + + it('should limit the time period in the query', async () => { + const callCluster = jest.fn(); + const clusters = [{ clusterUuid, clusterName }]; + const index = '.monitoring-es-*'; + await fetchLicenses(callCluster, clusters, index); + const params = callCluster.mock.calls[0][1]; + expect(params.body.query.bool.filter[2].range.timestamp.gte).toBe('now-2m'); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts new file mode 100644 index 0000000000000..6cec7f3296926 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { AlertLicense, AlertCluster } from '../../../common/types/alerts'; +import { ElasticsearchResponse } from '../../../common/types/es'; + +export async function fetchLicenses( + callCluster: any, + clusters: AlertCluster[], + index: string +): Promise { + const params = { + index, + filterPath: [ + 'hits.hits._source.license.*', + 'hits.hits._source.cluster_uuid', + 'hits.hits._index', + ], + body: { + size: clusters.length, + sort: [ + { + timestamp: { + order: 'desc', + unmapped_type: 'long', + }, + }, + ], + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), + }, + }, + { + term: { + type: 'cluster_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + collapse: { + field: 'cluster_uuid', + }, + }, + }; + + const response: ElasticsearchResponse = await callCluster('search', params); + return ( + response?.hits?.hits.map((hit) => { + const rawLicense = hit._source.license ?? {}; + const license: AlertLicense = { + status: rawLicense.status ?? '', + type: rawLicense.type ?? '', + expiryDateMS: rawLicense.expiry_date_in_millis ?? 0, + clusterUuid: hit._source.cluster_uuid, + ccs: hit._index, + }; + return license; + }) ?? [] + ); +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts new file mode 100644 index 0000000000000..a739593df27e9 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts @@ -0,0 +1,74 @@ +/* + * 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 { fetchLogstashVersions } from './fetch_logstash_versions'; + +describe('fetchLogstashVersions', () => { + let callCluster = jest.fn(); + const clusters = [ + { + clusterUuid: 'cluster123', + clusterName: 'test-cluster', + }, + ]; + const index = '.monitoring-logstash-*'; + const size = 10; + + it('fetch as expected', async () => { + callCluster = jest.fn().mockImplementation(() => { + return { + aggregations: { + index: { + buckets: [ + { + key: `Monitoring:${index}`, + }, + ], + }, + cluster: { + buckets: [ + { + key: 'cluster123', + group_by_logstash: { + buckets: [ + { + group_by_version: { + buckets: [ + { + key: '8.0.0', + }, + ], + }, + }, + { + group_by_version: { + buckets: [ + { + key: '7.2.1', + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + }; + }); + + const result = await fetchLogstashVersions(callCluster, clusters, index, size); + expect(result).toEqual([ + { + clusterUuid: clusters[0].clusterUuid, + ccs: 'Monitoring', + versions: ['8.0.0', '7.2.1'], + }, + ]); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts new file mode 100644 index 0000000000000..8f20c64d6243e --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts @@ -0,0 +1,111 @@ +/* + * 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 { get } from 'lodash'; +import { AlertCluster, AlertVersions } from '../../../common/types/alerts'; + +interface ESAggResponse { + key: string; +} + +export async function fetchLogstashVersions( + callCluster: any, + clusters: AlertCluster[], + index: string, + size: number +): Promise { + const params = { + index, + filterPath: ['aggregations'], + body: { + size: 0, + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), + }, + }, + { + term: { + type: 'logstash_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + aggs: { + index: { + terms: { + field: '_index', + size: 1, + }, + }, + cluster: { + terms: { + field: 'cluster_uuid', + size: 1, + }, + aggs: { + group_by_logstash: { + terms: { + field: 'logstash_stats.logstash.uuid', + size, + }, + aggs: { + group_by_version: { + terms: { + field: 'logstash_stats.logstash.version', + size: 1, + order: { + latest_report: 'desc', + }, + }, + aggs: { + latest_report: { + max: { + field: 'timestamp', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const response = await callCluster('search', params); + const indexName = get(response, 'aggregations.index.buckets[0].key', ''); + const clusterList = get(response, 'aggregations.cluster.buckets', []) as ESAggResponse[]; + return clusterList.map((cluster) => { + const clusterUuid = cluster.key; + const uuids = get(cluster, 'group_by_logstash.buckets', []); + const byVersion: { [version: string]: boolean } = {}; + for (const uuid of uuids) { + const version = get(uuid, 'group_by_version.buckets[0].key', ''); + if (!version) { + continue; + } + byVersion[version] = true; + } + return { + versions: Object.keys(byVersion), + clusterUuid, + ccs: indexName.includes(':') ? indexName.split(':')[0] : null, + }; + }); +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts new file mode 100644 index 0000000000000..c399594c170fa --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts @@ -0,0 +1,105 @@ +/* + * 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 { AlertCluster, AlertClusterStatsNodes } from '../../../common/types/alerts'; +import { ElasticsearchSource } from '../../../common/types/es'; + +function formatNode( + nodes: NonNullable['nodes']> | undefined +) { + if (!nodes) { + return []; + } + return Object.keys(nodes).map((nodeUuid) => { + return { + nodeUuid, + nodeEphemeralId: nodes[nodeUuid].ephemeral_id, + nodeName: nodes[nodeUuid].name, + }; + }); +} + +export async function fetchNodesFromClusterStats( + callCluster: any, + clusters: AlertCluster[], + index: string +): Promise { + const params = { + index, + filterPath: ['aggregations.clusters.buckets'], + body: { + size: 0, + sort: [ + { + timestamp: { + order: 'desc', + unmapped_type: 'long', + }, + }, + ], + query: { + bool: { + filter: [ + { + term: { + type: 'cluster_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + aggs: { + clusters: { + terms: { + include: clusters.map((cluster) => cluster.clusterUuid), + field: 'cluster_uuid', + }, + aggs: { + top: { + top_hits: { + sort: [ + { + timestamp: { + order: 'desc', + unmapped_type: 'long', + }, + }, + ], + _source: { + includes: ['cluster_state.nodes_hash', 'cluster_state.nodes'], + }, + size: 2, + }, + }, + }, + }, + }, + }, + }; + + const response = await callCluster('search', params); + const nodes = []; + const clusterBuckets = response.aggregations.clusters.buckets; + for (const clusterBucket of clusterBuckets) { + const clusterUuid = clusterBucket.key; + const hits = clusterBucket.top.hits.hits; + const indexName = hits[0]._index; + nodes.push({ + clusterUuid, + recentNodes: formatNode(hits[0]._source.cluster_state?.nodes), + priorNodes: formatNode(hits[1]._source.cluster_state?.nodes), + ccs: indexName.includes(':') ? indexName.split(':')[0] : undefined, + }); + } + return nodes; +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts index 6c08a0b3db758..399b26a6c5c31 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -28,7 +28,7 @@ export async function fetchStatus( await Promise.all( (alertTypes || ALERTS).map(async (type) => { const alert = await AlertsFactory.getByType(type, alertsClient); - if (!alert || !alert.isEnabled(licenseService) || !alert.rawAlert) { + if (!alert || !alert.rawAlert) { return; } diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts index a8389e26d4f9f..901ea96d525e8 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts @@ -25,8 +25,7 @@ export function enableAlertsRoute(_server: unknown, npRoute: RouteDependencies) }, async (context, request, response) => { try { - const alerts = AlertsFactory.getAll().filter((a) => a.isEnabled(npRoute.licenseService)); - + const alerts = AlertsFactory.getAll(); if (alerts.length) { const { isSufficientlySecure, diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index e9a9bb8146dbf..1db5f62823e9b 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -23,6 +23,7 @@ export { getCoreVitalsComponent, HeaderMenuPortal } from './components/shared/'; export { useTrackPageview, useUiTracker, + useTrackMetric, UiTracker, TrackMetricOptions, METRIC_TYPE, diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts index 4a6a1d61a9221..779454e9474ee 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts @@ -51,16 +51,8 @@ export const getDataFromSourceHits = ( { category: fieldCategory, field, - values: Array.isArray(item) - ? item.map((value) => { - if (isObject(value)) { - return JSON.stringify(value); - } - - return value; - }) - : [item], - originalValue: item, + values: toStringArray(item), + originalValue: toStringArray(item), } as TimelineEventsDetailsItem, ]; } else if (isObject(item)) { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6658671b84682..168eb14966493 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -14427,7 +14427,6 @@ "xpack.monitoring.alerts.clusterHealth.firing.internalFullMessage": "クラスター正常性アラートが{clusterName}に対して作動しています。現在の正常性は{health}です。{action}", "xpack.monitoring.alerts.clusterHealth.firing.internalShortMessage": "クラスター正常性アラートが{clusterName}に対して作動しています。現在の正常性は{health}です。{actionText}", "xpack.monitoring.alerts.clusterHealth.label": "クラスターの正常性", - "xpack.monitoring.alerts.clusterHealth.nodeNameLabel": "Elasticsearch クラスターアラート", "xpack.monitoring.alerts.clusterHealth.redMessage": "見つからないプライマリおよびレプリカシャードを割り当て", "xpack.monitoring.alerts.clusterHealth.ui.firingMessage": "Elasticsearchクラスターの正常性は{health}です。", "xpack.monitoring.alerts.clusterHealth.ui.nextSteps.message1": "{message}. #start_linkView now#end_link", @@ -14467,7 +14466,6 @@ "xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage": "{clusterName}に対してElasticsearchバージョン不一致アラートが実行されています。{shortActionText}", "xpack.monitoring.alerts.elasticsearchVersionMismatch.fullAction": "ノードの表示", "xpack.monitoring.alerts.elasticsearchVersionMismatch.label": "Elasticsearch バージョン不一致", - "xpack.monitoring.alerts.elasticsearchVersionMismatch.nodeNameLabel": "Elasticsearch ノードアラート", "xpack.monitoring.alerts.elasticsearchVersionMismatch.shortAction": "すべてのノードのバージョンが同じことを確認してください。", "xpack.monitoring.alerts.elasticsearchVersionMismatch.ui.firingMessage": "このクラスターでは、複数のバージョンの Elasticsearch({versions})が実行されています。", "xpack.monitoring.alerts.flyoutExpressions.timeUnits.dayLabel": "{timeValue, plural, other {日}}", @@ -14481,7 +14479,6 @@ "xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalShortMessage": "{clusterName}に対してKibanaバージョン不一致アラートが実行されています。{shortActionText}", "xpack.monitoring.alerts.kibanaVersionMismatch.fullAction": "インスタンスを表示", "xpack.monitoring.alerts.kibanaVersionMismatch.label": "Kibana バージョン不一致", - "xpack.monitoring.alerts.kibanaVersionMismatch.nodeNameLabel": "Kibana インスタンスアラート", "xpack.monitoring.alerts.kibanaVersionMismatch.shortAction": "すべてのインスタンスのバージョンが同じことを確認してください。", "xpack.monitoring.alerts.kibanaVersionMismatch.ui.firingMessage": "このクラスターでは、複数のバージョンの Kibana({versions})が実行されています。", "xpack.monitoring.alerts.legacyAlert.expressionText": "構成するものがありません。", @@ -14492,7 +14489,6 @@ "xpack.monitoring.alerts.licenseExpiration.firing.internalFullMessage": "ライセンス有効期限アラートが {clusterName} に対して実行されています。ライセンスは{expiredDate}に期限切れになります。{action}", "xpack.monitoring.alerts.licenseExpiration.firing.internalShortMessage": "ライセンス有効期限アラートが {clusterName} に対して実行されています。ライセンスは{expiredDate}に期限切れになります。{actionText}", "xpack.monitoring.alerts.licenseExpiration.label": "ライセンス期限", - "xpack.monitoring.alerts.licenseExpiration.nodeNameLabel": "Elasticsearch クラスターアラート", "xpack.monitoring.alerts.licenseExpiration.ui.firingMessage": "このクラスターのライセンスは#absoluteの#relativeに期限切れになります。#start_linkライセンスを更新してください。#end_link", "xpack.monitoring.alerts.logstashVersionMismatch.actionVariables.clusterHealth": "このクラスターを実行している Logstash のバージョン。", "xpack.monitoring.alerts.logstashVersionMismatch.description": "クラスターに複数のバージョンの Logstash があるときにアラートを発行します。", @@ -14500,7 +14496,6 @@ "xpack.monitoring.alerts.logstashVersionMismatch.firing.internalShortMessage": "{clusterName}に対してLogstashバージョン不一致アラートが実行されています。{shortActionText}", "xpack.monitoring.alerts.logstashVersionMismatch.fullAction": "ノードの表示", "xpack.monitoring.alerts.logstashVersionMismatch.label": "Logstash バージョン不一致", - "xpack.monitoring.alerts.logstashVersionMismatch.nodeNameLabel": "Logstash ノードアラート", "xpack.monitoring.alerts.logstashVersionMismatch.shortAction": "すべてのノードのバージョンが同じことを確認してください。", "xpack.monitoring.alerts.logstashVersionMismatch.ui.firingMessage": "このクラスターでは、複数のバージョンの Logstash({versions})が実行されています。", "xpack.monitoring.alerts.memoryUsage.actionVariables.count": "高メモリー使用率を報告しているノード数。", @@ -14543,7 +14538,6 @@ "xpack.monitoring.alerts.nodesChanged.firing.internalShortMessage": "{clusterName}に対してノード変更アラートが実行されています。{shortActionText}", "xpack.monitoring.alerts.nodesChanged.fullAction": "ノードの表示", "xpack.monitoring.alerts.nodesChanged.label": "ノードが変更されました", - "xpack.monitoring.alerts.nodesChanged.nodeNameLabel": "Elasticsearch ノードアラート", "xpack.monitoring.alerts.nodesChanged.shortAction": "ノードを追加、削除、または再起動したことを確認してください。", "xpack.monitoring.alerts.nodesChanged.ui.addedFiringMessage": "Elasticsearchノード「{added}」がこのクラスターに追加されました。", "xpack.monitoring.alerts.nodesChanged.ui.nothingDetectedFiringMessage": "Elasticsearchノードが変更されました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9602583e8d215..129deb575a52f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -14469,7 +14469,6 @@ "xpack.monitoring.alerts.clusterHealth.firing.internalFullMessage": "为 {clusterName} 触发了集群运行状况告警。当前运行状况为 {health}。{action}", "xpack.monitoring.alerts.clusterHealth.firing.internalShortMessage": "为 {clusterName} 触发了集群运行状况告警。当前运行状况为 {health}。{actionText}", "xpack.monitoring.alerts.clusterHealth.label": "集群运行状况", - "xpack.monitoring.alerts.clusterHealth.nodeNameLabel": "Elasticsearch 集群告警", "xpack.monitoring.alerts.clusterHealth.redMessage": "分配缺失的主分片和副本分片", "xpack.monitoring.alerts.clusterHealth.ui.firingMessage": "Elasticsearch 集群运行状况为 {health}。", "xpack.monitoring.alerts.clusterHealth.ui.nextSteps.message1": "{message}。#start_link立即查看#end_link", @@ -14509,7 +14508,6 @@ "xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage": "为 {clusterName} 触发了 Elasticsearch 版本不匹配告警。{shortActionText}", "xpack.monitoring.alerts.elasticsearchVersionMismatch.fullAction": "查看节点", "xpack.monitoring.alerts.elasticsearchVersionMismatch.label": "Elasticsearch 版本不匹配", - "xpack.monitoring.alerts.elasticsearchVersionMismatch.nodeNameLabel": "Elasticsearch 节点告警", "xpack.monitoring.alerts.elasticsearchVersionMismatch.shortAction": "确认所有节点具有相同的版本。", "xpack.monitoring.alerts.elasticsearchVersionMismatch.ui.firingMessage": "在此集群中正运行着多个 Elasticsearch ({versions}) 版本。", "xpack.monitoring.alerts.flyoutExpressions.timeUnits.dayLabel": "{timeValue, plural, other {天}}", @@ -14523,7 +14521,6 @@ "xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalShortMessage": "为 {clusterName} 触发了 Kibana 版本不匹配告警。{shortActionText}", "xpack.monitoring.alerts.kibanaVersionMismatch.fullAction": "查看实例", "xpack.monitoring.alerts.kibanaVersionMismatch.label": "Kibana 版本不匹配", - "xpack.monitoring.alerts.kibanaVersionMismatch.nodeNameLabel": "Kibana 实例告警", "xpack.monitoring.alerts.kibanaVersionMismatch.shortAction": "确认所有实例具有相同的版本。", "xpack.monitoring.alerts.kibanaVersionMismatch.ui.firingMessage": "在此集群中正运行着多个 Kibana 版本 ({versions})。", "xpack.monitoring.alerts.legacyAlert.expressionText": "没有可配置的内容。", @@ -14534,7 +14531,6 @@ "xpack.monitoring.alerts.licenseExpiration.firing.internalFullMessage": "为 {clusterName} 触发了许可证到期告警。您的许可证将于 {expiredDate}到期。{action}", "xpack.monitoring.alerts.licenseExpiration.firing.internalShortMessage": "为 {clusterName} 触发了许可证到期告警。您的许可证将于 {expiredDate}到期。{actionText}", "xpack.monitoring.alerts.licenseExpiration.label": "许可证到期", - "xpack.monitoring.alerts.licenseExpiration.nodeNameLabel": "Elasticsearch 集群告警", "xpack.monitoring.alerts.licenseExpiration.ui.firingMessage": "此集群的许可证将于 #relative后,即 #absolute到期。 #start_link请更新您的许可证。#end_link", "xpack.monitoring.alerts.logstashVersionMismatch.actionVariables.clusterHealth": "此集群中运行的 Logstash 版本。", "xpack.monitoring.alerts.logstashVersionMismatch.description": "集群包含多个版本的 Logstash 时告警。", @@ -14542,7 +14538,6 @@ "xpack.monitoring.alerts.logstashVersionMismatch.firing.internalShortMessage": "为 {clusterName} 触发了 Logstash 版本不匹配告警。{shortActionText}", "xpack.monitoring.alerts.logstashVersionMismatch.fullAction": "查看节点", "xpack.monitoring.alerts.logstashVersionMismatch.label": "Logstash 版本不匹配", - "xpack.monitoring.alerts.logstashVersionMismatch.nodeNameLabel": "Logstash 节点告警", "xpack.monitoring.alerts.logstashVersionMismatch.shortAction": "确认所有节点具有相同的版本。", "xpack.monitoring.alerts.logstashVersionMismatch.ui.firingMessage": "在此集群中正运行着多个 Logstash 版本 ({versions})。", "xpack.monitoring.alerts.memoryUsage.actionVariables.count": "报告高内存使用率的节点数目。", @@ -14585,7 +14580,6 @@ "xpack.monitoring.alerts.nodesChanged.firing.internalShortMessage": "为 {clusterName} 触发了节点已更改告警。{shortActionText}", "xpack.monitoring.alerts.nodesChanged.fullAction": "查看节点", "xpack.monitoring.alerts.nodesChanged.label": "节点已更改", - "xpack.monitoring.alerts.nodesChanged.nodeNameLabel": "Elasticsearch 节点告警", "xpack.monitoring.alerts.nodesChanged.shortAction": "确认您已添加、移除或重新启动节点。", "xpack.monitoring.alerts.nodesChanged.ui.addedFiringMessage": "Elasticsearch 节点“{added}”已添加到此集群。", "xpack.monitoring.alerts.nodesChanged.ui.nothingDetectedFiringMessage": "Elasticsearch 节点已更改", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx index 2d47740a581b8..ea1bcf82c314c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx @@ -96,7 +96,7 @@ describe('jira action params validation', () => { }; expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: { 'subActionParams.incident.summary': [] }, + errors: { 'subActionParams.incident.summary': [], 'subActionParams.incident.labels': [] }, }); }); @@ -108,6 +108,23 @@ describe('jira action params validation', () => { expect(actionTypeModel.validateParams(actionParams)).toEqual({ errors: { 'subActionParams.incident.summary': ['Summary is required.'], + 'subActionParams.incident.labels': [], + }, + }); + }); + + test('params validation fails when labels contain spaces', () => { + const actionParams = { + subActionParams: { + incident: { summary: 'some title', labels: ['label with spaces'] }, + comments: [], + }, + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + 'subActionParams.incident.summary': [], + 'subActionParams.incident.labels': ['Labels cannot contain spaces.'], }, }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx index 5cb8a76d09bee..26b37278003c3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx @@ -72,6 +72,7 @@ export function getActionType(): ActionTypeModel => { const errors = { 'subActionParams.incident.summary': new Array(), + 'subActionParams.incident.labels': new Array(), }; const validationResult = { errors, @@ -83,6 +84,12 @@ export function getActionType(): ActionTypeModel label.match(/\s/g))) + errors['subActionParams.incident.labels'].push(i18n.LABELS_WHITE_SPACES); + } return validationResult; }, actionParamsFields: lazy(() => import('./jira_params')), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx index 75930482797a2..cb2d637972cb8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -184,6 +184,11 @@ const JiraParamsFields: React.FunctionComponent 0 && + incident.labels !== undefined; + return ( <> @@ -304,6 +309,8 @@ const JiraParamsFields: React.FunctionComponent diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts index 3c8bda7792f0a..fe7ea61e68193 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts @@ -199,3 +199,10 @@ export const SEARCH_ISSUES_LOADING = i18n.translate( defaultMessage: 'Loading...', } ); + +export const LABELS_WHITE_SPACES = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.labelsSpacesErrorMessage', + { + defaultMessage: 'Labels cannot contain spaces.', + } +); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx index d77349e53b354..fa6badb34635b 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx @@ -190,6 +190,7 @@ export class UpgradeAssistantTabs extends React.Component { return [ { id: 'overview', + 'data-test-subj': 'upgradeAssistantOverviewTab', name: i18n.translate('xpack.upgradeAssistant.overviewTab.overviewTabTitle', { defaultMessage: 'Overview', }), @@ -197,6 +198,7 @@ export class UpgradeAssistantTabs extends React.Component { }, { id: 'cluster', + 'data-test-subj': 'upgradeAssistantClusterTab', name: i18n.translate('xpack.upgradeAssistant.checkupTab.clusterTabLabel', { defaultMessage: 'Cluster', }), @@ -213,6 +215,7 @@ export class UpgradeAssistantTabs extends React.Component { }, { id: 'indices', + 'data-test-subj': 'upgradeAssistantIndicesTab', name: i18n.translate('xpack.upgradeAssistant.checkupTab.indicesTabLabel', { defaultMessage: 'Indices', }), diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap index 5aa4a469e4f02..bac67bf722ea7 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap @@ -6,7 +6,9 @@ exports[`CheckupTab render with deprecations 1`] = ` -

+

-

+

-

+

= ({ <> -

+

= (props) <> - +

; +export type TLSActionGroup = ActionGroup<'xpack.uptime.alerts.actionGroups.tls'>; +export type DurationAnomalyActionGroup = ActionGroup<'xpack.uptime.alerts.actionGroups.durationAnomaly'>; + +export const MONITOR_STATUS: MonitorStatusActionGroup = { + id: 'xpack.uptime.alerts.actionGroups.monitorStatus', + name: 'Uptime Down Monitor', +}; + +export const TLS: TLSActionGroup = { + id: 'xpack.uptime.alerts.actionGroups.tls', + name: 'Uptime TLS Alert', +}; + +export const DURATION_ANOMALY: DurationAnomalyActionGroup = { + id: 'xpack.uptime.alerts.actionGroups.durationAnomaly', + name: 'Uptime Duration Anomaly', +}; + export const ACTION_GROUP_DEFINITIONS: { - MONITOR_STATUS: ActionGroup<'xpack.uptime.alerts.actionGroups.monitorStatus'>; - TLS: ActionGroup<'xpack.uptime.alerts.actionGroups.tls'>; - DURATION_ANOMALY: ActionGroup<'xpack.uptime.alerts.actionGroups.durationAnomaly'>; + MONITOR_STATUS: MonitorStatusActionGroup; + TLS: TLSActionGroup; + DURATION_ANOMALY: DurationAnomalyActionGroup; } = { - MONITOR_STATUS: { - id: 'xpack.uptime.alerts.actionGroups.monitorStatus', - name: 'Uptime Down Monitor', - }, - TLS: { - id: 'xpack.uptime.alerts.actionGroups.tls', - name: 'Uptime TLS Alert', - }, - DURATION_ANOMALY: { - id: 'xpack.uptime.alerts.actionGroups.durationAnomaly', - name: 'Uptime Duration Anomaly', - }, + MONITOR_STATUS, + TLS, + DURATION_ANOMALY, }; export const CLIENT_ALERT_TYPES = { diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts index 487daf0332a98..a02116877f49a 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts @@ -5,10 +5,143 @@ * 2.0. */ -import { colourPalette, getSeriesAndDomain } from './data_formatting'; +import { colourPalette, getSeriesAndDomain, getSidebarItems } from './data_formatting'; import { NetworkItems, MimeType } from './types'; import { WaterfallDataEntry } from '../../waterfall/types'; +const networkItems: NetworkItems = [ + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'https://unpkg.com/todomvc-app-css@2.0.4/index.css', + status: 200, + mimeType: 'text/css', + requestSentTime: 18098833.175, + requestStartTime: 18098835.439, + loadEndTime: 18098957.145, + timings: { + connect: 81.10800000213203, + wait: 34.577999998873565, + receive: 0.5520000013348181, + send: 0.3600000018195715, + total: 123.97000000055414, + proxy: -1, + blocked: 0.8540000017092098, + queueing: 2.263999998831423, + ssl: 55.38700000033714, + dns: 3.559999997378327, + }, + }, + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'https://unpkg.com/director@1.2.8/build/director.js', + status: 200, + mimeType: 'application/javascript', + requestSentTime: 18098833.537, + requestStartTime: 18098837.233999997, + loadEndTime: 18098977.648000002, + timings: { + blocked: 84.54599999822676, + receive: 3.068000001803739, + queueing: 3.69700000010198, + proxy: -1, + total: 144.1110000014305, + wait: 52.56100000042352, + connect: -1, + send: 0.2390000008745119, + ssl: -1, + dns: -1, + }, + }, +]; + +const networkItemsWithoutFullTimings: NetworkItems = [ + networkItems[0], + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', + status: 0, + mimeType: 'text/javascript', + requestSentTime: 18098834.097, + loadEndTime: 18098836.889999997, + timings: { + total: 2.7929999996558763, + blocked: -1, + ssl: -1, + wait: -1, + connect: -1, + dns: -1, + queueing: -1, + send: -1, + proxy: -1, + receive: -1, + }, + }, +]; + +const networkItemsWithoutAnyTimings: NetworkItems = [ + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', + status: 0, + mimeType: 'text/javascript', + requestSentTime: 18098834.097, + loadEndTime: 18098836.889999997, + timings: { + total: -1, + blocked: -1, + ssl: -1, + wait: -1, + connect: -1, + dns: -1, + queueing: -1, + send: -1, + proxy: -1, + receive: -1, + }, + }, +]; + +const networkItemsWithoutTimingsObject: NetworkItems = [ + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', + status: 0, + mimeType: 'text/javascript', + requestSentTime: 18098834.097, + loadEndTime: 18098836.889999997, + }, +]; + +const networkItemsWithUncommonMimeType: NetworkItems = [ + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'https://unpkg.com/director@1.2.8/build/director.js', + status: 200, + mimeType: 'application/x-javascript', + requestSentTime: 18098833.537, + requestStartTime: 18098837.233999997, + loadEndTime: 18098977.648000002, + timings: { + blocked: 84.54599999822676, + receive: 3.068000001803739, + queueing: 3.69700000010198, + proxy: -1, + total: 144.1110000014305, + wait: 52.56100000042352, + connect: -1, + send: 0.2390000008745119, + ssl: -1, + dns: -1, + }, + }, +]; + describe('Palettes', () => { it('A colour palette comprising timing and mime type colours is correctly generated', () => { expect(colourPalette).toEqual({ @@ -30,139 +163,6 @@ describe('Palettes', () => { }); describe('getSeriesAndDomain', () => { - const networkItems: NetworkItems = [ - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'https://unpkg.com/todomvc-app-css@2.0.4/index.css', - status: 200, - mimeType: 'text/css', - requestSentTime: 18098833.175, - requestStartTime: 18098835.439, - loadEndTime: 18098957.145, - timings: { - connect: 81.10800000213203, - wait: 34.577999998873565, - receive: 0.5520000013348181, - send: 0.3600000018195715, - total: 123.97000000055414, - proxy: -1, - blocked: 0.8540000017092098, - queueing: 2.263999998831423, - ssl: 55.38700000033714, - dns: 3.559999997378327, - }, - }, - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'https://unpkg.com/director@1.2.8/build/director.js', - status: 200, - mimeType: 'application/javascript', - requestSentTime: 18098833.537, - requestStartTime: 18098837.233999997, - loadEndTime: 18098977.648000002, - timings: { - blocked: 84.54599999822676, - receive: 3.068000001803739, - queueing: 3.69700000010198, - proxy: -1, - total: 144.1110000014305, - wait: 52.56100000042352, - connect: -1, - send: 0.2390000008745119, - ssl: -1, - dns: -1, - }, - }, - ]; - - const networkItemsWithoutFullTimings: NetworkItems = [ - networkItems[0], - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', - status: 0, - mimeType: 'text/javascript', - requestSentTime: 18098834.097, - loadEndTime: 18098836.889999997, - timings: { - total: 2.7929999996558763, - blocked: -1, - ssl: -1, - wait: -1, - connect: -1, - dns: -1, - queueing: -1, - send: -1, - proxy: -1, - receive: -1, - }, - }, - ]; - - const networkItemsWithoutAnyTimings: NetworkItems = [ - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', - status: 0, - mimeType: 'text/javascript', - requestSentTime: 18098834.097, - loadEndTime: 18098836.889999997, - timings: { - total: -1, - blocked: -1, - ssl: -1, - wait: -1, - connect: -1, - dns: -1, - queueing: -1, - send: -1, - proxy: -1, - receive: -1, - }, - }, - ]; - - const networkItemsWithoutTimingsObject: NetworkItems = [ - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', - status: 0, - mimeType: 'text/javascript', - requestSentTime: 18098834.097, - loadEndTime: 18098836.889999997, - }, - ]; - - const networkItemsWithUncommonMimeType: NetworkItems = [ - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'https://unpkg.com/director@1.2.8/build/director.js', - status: 200, - mimeType: 'application/x-javascript', - requestSentTime: 18098833.537, - requestStartTime: 18098837.233999997, - loadEndTime: 18098977.648000002, - timings: { - blocked: 84.54599999822676, - receive: 3.068000001803739, - queueing: 3.69700000010198, - proxy: -1, - total: 144.1110000014305, - wait: 52.56100000042352, - connect: -1, - send: 0.2390000008745119, - ssl: -1, - dns: -1, - }, - }, - ]; - it('formats timings', () => { const actual = getSeriesAndDomain(networkItems); expect(actual).toMatchInlineSnapshot(` @@ -175,6 +175,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#dcd4c4", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#dcd4c4", @@ -188,6 +189,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#54b399", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#54b399", @@ -201,6 +203,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#da8b45", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#da8b45", @@ -214,6 +217,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#edc5a2", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#edc5a2", @@ -227,6 +231,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#d36086", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#d36086", @@ -240,6 +245,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#b0c9e0", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#b0c9e0", @@ -253,6 +259,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#ca8eae", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#ca8eae", @@ -266,6 +273,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#dcd4c4", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#dcd4c4", @@ -279,6 +287,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#d36086", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#d36086", @@ -292,6 +301,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#b0c9e0", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#b0c9e0", @@ -305,6 +315,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#9170b8", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#9170b8", @@ -316,6 +327,7 @@ describe('getSeriesAndDomain', () => { "y0": 137.70799999925657, }, ], + "totalHighlightedRequests": 2, } `); }); @@ -332,6 +344,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#dcd4c4", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#dcd4c4", @@ -345,6 +358,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#54b399", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#54b399", @@ -358,6 +372,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#da8b45", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#da8b45", @@ -371,6 +386,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#edc5a2", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#edc5a2", @@ -384,6 +400,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#d36086", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#d36086", @@ -397,6 +414,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#b0c9e0", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#b0c9e0", @@ -410,6 +428,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#ca8eae", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#ca8eae", @@ -423,6 +442,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#9170b8", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#9170b8", @@ -434,6 +454,7 @@ describe('getSeriesAndDomain', () => { "y0": 0.9219999983906746, }, ], + "totalHighlightedRequests": 2, } `); }); @@ -450,6 +471,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "", + "isHighlighted": true, "showTooltip": false, "tooltipProps": undefined, }, @@ -458,6 +480,7 @@ describe('getSeriesAndDomain', () => { "y0": 0, }, ], + "totalHighlightedRequests": 1, } `); }); @@ -473,6 +496,7 @@ describe('getSeriesAndDomain', () => { "series": Array [ Object { "config": Object { + "isHighlighted": true, "showTooltip": false, }, "x": 0, @@ -480,6 +504,7 @@ describe('getSeriesAndDomain', () => { "y0": 0, }, ], + "totalHighlightedRequests": 1, } `); }); @@ -501,4 +526,41 @@ describe('getSeriesAndDomain', () => { }); expect(contentDownloadedingConfigItem).toBeDefined(); }); + + it('counts the total number of highlighted items', () => { + // only one CSS file in this array of network Items + const actual = getSeriesAndDomain(networkItems, false, '', ['stylesheet']); + expect(actual.totalHighlightedRequests).toBe(1); + }); + + it('adds isHighlighted to waterfall entry when filter matches', () => { + // only one CSS file in this array of network Items + const { series } = getSeriesAndDomain(networkItems, false, '', ['stylesheet']); + series.forEach((item) => { + if (item.x === 0) { + expect(item.config.isHighlighted).toBe(true); + } else { + expect(item.config.isHighlighted).toBe(false); + } + }); + }); + + it('adds isHighlighted to waterfall entry when query matches', () => { + // only the second item matches this query + const { series } = getSeriesAndDomain(networkItems, false, 'director', []); + series.forEach((item) => { + if (item.x === 1) { + expect(item.config.isHighlighted).toBe(true); + } else { + expect(item.config.isHighlighted).toBe(false); + } + }); + }); +}); + +describe('getSidebarItems', () => { + it('passes the item index offset by 1 to offsetIndex for visual display', () => { + const actual = getSidebarItems(networkItems, false, '', []); + expect(actual[0].offsetIndex).toBe(1); + }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts index 0ac93794594c0..46f0d23d0a6b9 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts @@ -55,8 +55,28 @@ const getFriendlyTooltipValue = ({ } return `${label}: ${formatValueForDisplay(value)}ms`; }; +export const isHighlightedItem = ( + item: NetworkItem, + query?: string, + activeFilters: string[] = [] +) => { + if (!query && activeFilters?.length === 0) { + return true; + } + + const matchQuery = query ? item.url?.includes(query) : true; + const matchFilters = + activeFilters.length > 0 ? activeFilters.includes(MimeTypesMap[item.mimeType!]) : true; + + return !!(matchQuery && matchFilters); +}; -export const getSeriesAndDomain = (items: NetworkItems) => { +export const getSeriesAndDomain = ( + items: NetworkItems, + onlyHighlighted = false, + query?: string, + activeFilters?: string[] +) => { const getValueForOffset = (item: NetworkItem) => { return item.requestSentTime; }; @@ -78,13 +98,21 @@ export const getSeriesAndDomain = (items: NetworkItems) => { } }; + let totalHighlightedRequests = 0; + const series = items.reduce((acc, item, index) => { + const isHighlighted = isHighlightedItem(item, query, activeFilters); + if (isHighlighted) { + totalHighlightedRequests++; + } + if (!item.timings) { acc.push({ x: index, y0: 0, y: 0, config: { + isHighlighted, showTooltip: false, }, }); @@ -96,10 +124,13 @@ export const getSeriesAndDomain = (items: NetworkItems) => { let currentOffset = offsetValue - zeroOffset; + let timingValueFound = false; + TIMING_ORDER.forEach((timing) => { const value = getValue(item.timings, timing); - const colour = timing === Timings.Receive ? mimeTypeColour : colourPalette[timing]; if (value && value >= 0) { + timingValueFound = true; + const colour = timing === Timings.Receive ? mimeTypeColour : colourPalette[timing]; const y = currentOffset + value; acc.push({ @@ -108,6 +139,7 @@ export const getSeriesAndDomain = (items: NetworkItems) => { y, config: { colour, + isHighlighted, showTooltip: true, tooltipProps: { value: getFriendlyTooltipValue({ @@ -126,7 +158,7 @@ export const getSeriesAndDomain = (items: NetworkItems) => { /* if no specific timing values are found, use the total time * if total time is not available use 0, set showTooltip to false, * and omit tooltip props */ - if (!acc.find((entry) => entry.x === index)) { + if (!timingValueFound) { const total = item.timings.total; const hasTotal = total !== -1; acc.push({ @@ -134,6 +166,7 @@ export const getSeriesAndDomain = (items: NetworkItems) => { y0: hasTotal ? currentOffset : 0, y: hasTotal ? currentOffset + item.timings.total : 0, config: { + isHighlighted, colour: hasTotal ? mimeTypeColour : '', showTooltip: hasTotal, tooltipProps: hasTotal @@ -154,14 +187,31 @@ export const getSeriesAndDomain = (items: NetworkItems) => { const yValues = series.map((serie) => serie.y); const domain = { min: 0, max: Math.max(...yValues) }; - return { series, domain }; + + let filteredSeries = series; + if (onlyHighlighted) { + filteredSeries = series.filter((item) => item.config.isHighlighted); + } + + return { series: filteredSeries, domain, totalHighlightedRequests }; }; -export const getSidebarItems = (items: NetworkItems): SidebarItems => { - return items.map((item) => { +export const getSidebarItems = ( + items: NetworkItems, + onlyHighlighted: boolean, + query: string, + activeFilters: string[] +): SidebarItems => { + const sideBarItems = items.map((item, index) => { + const isHighlighted = isHighlightedItem(item, query, activeFilters); + const offsetIndex = index + 1; const { url, status, method } = item; - return { url, status, method }; + return { url, status, method, isHighlighted, offsetIndex }; }); + if (onlyHighlighted) { + return sideBarItems.filter((item) => item.isHighlighted); + } + return sideBarItems; }; export const getLegendItems = (): LegendItems => { @@ -184,6 +234,7 @@ export const getLegendItems = (): LegendItems => { { name: FriendlyMimetypeLabels[mimeType], colour: MIME_TYPE_PALETTE[mimeType] }, ]; }); + return [...timingItems, ...mimeTypeItems]; }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts index 8d261edc74bf4..e22caae0d9eb2 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts @@ -61,16 +61,13 @@ export const TIMING_ORDER = [ Timings.Receive, ] as const; -export type CalculatedTimings = { - [K in Timings]?: number; -}; - export enum MimeType { Html = 'html', Script = 'script', Stylesheet = 'stylesheet', Media = 'media', Font = 'font', + XHR = 'xhr', Other = 'other', } @@ -99,6 +96,9 @@ export const FriendlyMimetypeLabels = { [MimeType.Font]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.font', { defaultMessage: 'Font', }), + [MimeType.XHR]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.xhr', { + defaultMessage: 'XHR', + }), [MimeType.Other]: i18n.translate( 'xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.other', { @@ -112,7 +112,6 @@ export const FriendlyMimetypeLabels = { export const MimeTypesMap: Record = { 'text/html': MimeType.Html, 'application/javascript': MimeType.Script, - 'application/json': MimeType.Script, 'text/javascript': MimeType.Script, 'text/css': MimeType.Stylesheet, // Images @@ -146,38 +145,18 @@ export const MimeTypesMap: Record = { 'application/font-woff2': MimeType.Font, 'application/vnd.ms-fontobject': MimeType.Font, 'application/font-sfnt': MimeType.Font, + + // XHR + 'application/json': MimeType.XHR, }; export type NetworkItem = NetworkEvent; export type NetworkItems = NetworkItem[]; -// NOTE: A number will always be present if the property exists, but that number might be -1, which represents no value. -export interface PayloadTimings { - dns_start: number; - push_end: number; - worker_fetch_start: number; - worker_respond_with_settled: number; - proxy_end: number; - worker_start: number; - worker_ready: number; - send_end: number; - connect_end: number; - connect_start: number; - send_start: number; - proxy_start: number; - push_start: number; - ssl_end: number; - receive_headers_end: number; - ssl_start: number; - request_time: number; - dns_end: number; -} - -export interface ExtraSeriesConfig { - colour: string; -} - -export type SidebarItem = Pick; +export type SidebarItem = Pick & { + isHighlighted: boolean; + offsetIndex: number; +}; export type SidebarItems = SidebarItem[]; export interface LegendItem { diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx new file mode 100644 index 0000000000000..e22f4a4c63f59 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx @@ -0,0 +1,248 @@ +/* + * 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 { act, fireEvent } from '@testing-library/react'; +import { WaterfallChartWrapper } from './waterfall_chart_wrapper'; + +import { render } from '../../../../../lib/helper/rtl_helpers'; + +import { extractItems, isHighlightedItem } from './data_formatting'; + +import 'jest-canvas-mock'; +import { BAR_HEIGHT } from '../../waterfall/components/constants'; +import { MimeType } from './types'; +import { + FILTER_POPOVER_OPEN_LABEL, + FILTER_REQUESTS_LABEL, + FILTER_COLLAPSE_REQUESTS_LABEL, +} from '../../waterfall/components/translations'; + +const getHighLightedItems = (query: string, filters: string[]) => { + return NETWORK_EVENTS.events.filter((item) => isHighlightedItem(item, query, filters)); +}; + +describe('waterfall chart wrapper', () => { + jest.useFakeTimers(); + + it('renders the correct sidebar items', () => { + const { getAllByTestId } = render( + + ); + + const sideBarItems = getAllByTestId('middleTruncatedTextSROnly'); + + expect(sideBarItems).toHaveLength(5); + }); + + it('search by query works', () => { + const { getAllByTestId, getByTestId, getByLabelText } = render( + + ); + + const filterInput = getByLabelText(FILTER_REQUESTS_LABEL); + + const searchText = '.js'; + + fireEvent.change(filterInput, { target: { value: searchText } }); + + // inout has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + const highlightedItemsLength = getHighLightedItems(searchText, []).length; + expect(getAllByTestId('sideBarHighlightedItem')).toHaveLength(highlightedItemsLength); + + expect(getAllByTestId('sideBarDimmedItem')).toHaveLength( + NETWORK_EVENTS.events.length - highlightedItemsLength + ); + + const SIDE_BAR_ITEMS_HEIGHT = NETWORK_EVENTS.events.length * BAR_HEIGHT; + expect(getByTestId('wfSidebarContainer')).toHaveAttribute('height', `${SIDE_BAR_ITEMS_HEIGHT}`); + + expect(getByTestId('wfDataOnlyBarChart')).toHaveAttribute('height', `${SIDE_BAR_ITEMS_HEIGHT}`); + }); + + it('search by mime type works', () => { + const { getAllByTestId, getByLabelText, getAllByText } = render( + + ); + + const sideBarItems = getAllByTestId('middleTruncatedTextSROnly'); + + expect(sideBarItems).toHaveLength(5); + + fireEvent.click(getByLabelText(FILTER_POPOVER_OPEN_LABEL)); + + fireEvent.click(getAllByText('XHR')[1]); + + // inout has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + const highlightedItemsLength = getHighLightedItems('', [MimeType.XHR]).length; + + expect(getAllByTestId('sideBarHighlightedItem')).toHaveLength(highlightedItemsLength); + expect(getAllByTestId('sideBarDimmedItem')).toHaveLength( + NETWORK_EVENTS.events.length - highlightedItemsLength + ); + }); + + it('renders sidebar even when filter matches 0 resources', () => { + const { getAllByTestId, getByLabelText, getAllByText, queryAllByTestId } = render( + + ); + + const sideBarItems = getAllByTestId('middleTruncatedTextSROnly'); + + expect(sideBarItems).toHaveLength(5); + + fireEvent.click(getByLabelText(FILTER_POPOVER_OPEN_LABEL)); + + fireEvent.click(getAllByText('CSS')[1]); + + // inout has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + const highlightedItemsLength = getHighLightedItems('', [MimeType.Stylesheet]).length; + + // no CSS items found + expect(queryAllByTestId('sideBarHighlightedItem')).toHaveLength(0); + expect(getAllByTestId('sideBarDimmedItem')).toHaveLength( + NETWORK_EVENTS.events.length - highlightedItemsLength + ); + + fireEvent.click(getByLabelText(FILTER_COLLAPSE_REQUESTS_LABEL)); + + // filter bar is still accessible even when no resources match filter + expect(getByLabelText(FILTER_REQUESTS_LABEL)).toBeInTheDocument(); + + // no resources items are in the chart as none match filter + expect(queryAllByTestId('sideBarHighlightedItem')).toHaveLength(0); + expect(queryAllByTestId('sideBarDimmedItem')).toHaveLength(0); + }); +}); + +const NETWORK_EVENTS = { + events: [ + { + timestamp: '2021-01-21T10:31:21.537Z', + method: 'GET', + url: + 'https://apv-static.minute.ly/videos/v-c2a526c7-450d-428e-1244649-a390-fb639ffead96-s45.746-54.421m.mp4', + status: 206, + mimeType: 'video/mp4', + requestSentTime: 241114127.474, + requestStartTime: 241114129.214, + loadEndTime: 241116573.402, + timings: { + total: 2445.928000001004, + queueing: 1.7399999778717756, + blocked: 0.391999987186864, + receive: 2283.964000031119, + connect: 91.5709999972023, + wait: 28.795999998692423, + proxy: -1, + dns: 36.952000024029985, + send: 0.10000000474974513, + ssl: 64.28900000173599, + }, + }, + { + timestamp: '2021-01-21T10:31:22.174Z', + method: 'GET', + url: 'https://dpm.demdex.net/ibs:dpid=73426&dpuuid=31597189268188866891125449924942215949', + status: 200, + mimeType: 'image/gif', + requestSentTime: 241114749.202, + requestStartTime: 241114750.426, + loadEndTime: 241114805.541, + timings: { + queueing: 1.2240000069141388, + receive: 2.218999987235293, + proxy: -1, + dns: -1, + send: 0.14200000441633165, + blocked: 1.033000007737428, + total: 56.33900000248104, + wait: 51.72099999617785, + ssl: -1, + connect: -1, + }, + }, + { + timestamp: '2021-01-21T10:31:21.679Z', + method: 'GET', + url: 'https://dapi.cms.mlbinfra.com/v2/content/en-us/sel-t119-homepage-mediawall', + status: 200, + mimeType: 'application/json', + requestSentTime: 241114268.04299998, + requestStartTime: 241114270.184, + loadEndTime: 241114665.609, + timings: { + total: 397.5659999996424, + dns: 29.5429999823682, + wait: 221.6830000106711, + queueing: 2.1410000044852495, + connect: 106.95499999565072, + ssl: 69.06899999012239, + receive: 2.027999988058582, + blocked: 0.877000013133511, + send: 23.719999997410923, + proxy: -1, + }, + }, + { + timestamp: '2021-01-21T10:31:21.740Z', + method: 'GET', + url: 'https://platform.twitter.com/embed/embed.runtime.b313577971db9c857801.js', + status: 200, + mimeType: 'application/javascript', + requestSentTime: 241114303.84899998, + requestStartTime: 241114306.416, + loadEndTime: 241114370.361, + timings: { + send: 1.357000001007691, + wait: 40.12299998430535, + receive: 16.78500001435168, + ssl: -1, + queueing: 2.5670000177342445, + total: 66.51200001942925, + connect: -1, + blocked: 5.680000002030283, + proxy: -1, + dns: -1, + }, + }, + { + timestamp: '2021-01-21T10:31:21.740Z', + method: 'GET', + url: 'https://platform.twitter.com/embed/embed.modules.7a266e7acfd42f2581a5.js', + status: 200, + mimeType: 'application/javascript', + requestSentTime: 241114305.939, + requestStartTime: 241114310.393, + loadEndTime: 241114938.264, + timings: { + wait: 51.61500000394881, + dns: -1, + ssl: -1, + receive: 506.5750000067055, + proxy: -1, + connect: -1, + blocked: 69.51599998865277, + queueing: 4.453999979887158, + total: 632.324999984121, + send: 0.16500000492669642, + }, + }, + ], +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx index 91657981e7f89..8a0e9729a635b 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx @@ -5,44 +5,14 @@ * 2.0. */ -import React, { useMemo, useState } from 'react'; -import { EuiHealth, EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; +import { EuiHealth } from '@elastic/eui'; +import { useTrackMetric, METRIC_TYPE } from '../../../../../../../observability/public'; import { getSeriesAndDomain, getSidebarItems, getLegendItems } from './data_formatting'; import { SidebarItem, LegendItem, NetworkItems } from './types'; -import { - WaterfallProvider, - WaterfallChart, - MiddleTruncatedText, - RenderItem, -} from '../../waterfall'; - -export const renderSidebarItem: RenderItem = (item, index) => { - const { status } = item; - - const isErrorStatusCode = (statusCode: number) => { - const is400 = statusCode >= 400 && statusCode <= 499; - const is500 = statusCode >= 500 && statusCode <= 599; - const isSpecific300 = statusCode === 301 || statusCode === 307 || statusCode === 308; - return is400 || is500 || isSpecific300; - }; - - return ( - <> - {!status || !isErrorStatusCode(status) ? ( - - ) : ( - - - - - - {status} - - - )} - - ); -}; +import { WaterfallProvider, WaterfallChart, RenderItem } from '../../waterfall'; +import { WaterfallFilter } from './waterfall_filter'; +import { WaterfallSidebarItem } from './waterfall_sidebar_item'; export const renderLegendItem: RenderItem = (item) => { return {item.name}; @@ -54,23 +24,64 @@ interface Props { } export const WaterfallChartWrapper: React.FC = ({ data, total }) => { + const [query, setQuery] = useState(''); + const [activeFilters, setActiveFilters] = useState([]); + const [onlyHighlighted, setOnlyHighlighted] = useState(false); + const [networkData] = useState(data); - const { series, domain } = useMemo(() => { - return getSeriesAndDomain(networkData); - }, [networkData]); + const hasFilters = activeFilters.length > 0; + + const { series, domain, totalHighlightedRequests } = useMemo(() => { + return getSeriesAndDomain(networkData, onlyHighlighted, query, activeFilters); + }, [networkData, query, activeFilters, onlyHighlighted]); const sidebarItems = useMemo(() => { - return getSidebarItems(networkData); - }, [networkData]); + return getSidebarItems(networkData, onlyHighlighted, query, activeFilters); + }, [networkData, query, activeFilters, onlyHighlighted]); const legendItems = getLegendItems(); + const renderFilter = useCallback(() => { + return ( + + ); + }, [activeFilters, setActiveFilters, onlyHighlighted, setOnlyHighlighted, query, setQuery]); + + const renderSidebarItem: RenderItem = useCallback( + (item) => { + return ( + + ); + }, + [hasFilters, onlyHighlighted] + ); + + useTrackMetric({ app: 'uptime', metric: 'waterfall_chart_view', metricType: METRIC_TYPE.COUNT }); + useTrackMetric({ + app: 'uptime', + metric: 'waterfall_chart_view', + metricType: METRIC_TYPE.COUNT, + delay: 15000, + }); + return ( { @@ -81,10 +92,19 @@ export const WaterfallChartWrapper: React.FC = ({ data, total }) => { tickFormat={(d: number) => `${Number(d).toFixed(0)} ms`} domain={domain} barStyleAccessor={(datum) => { + if (!datum.datum.config.isHighlighted) { + return { + rect: { + fill: datum.datum.config.colour, + opacity: '0.1', + }, + }; + } return datum.datum.config.colour; }} renderSidebarItem={renderSidebarItem} renderLegendItem={renderLegendItem} + renderFilter={renderFilter} fullHeight={true} /> diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.test.tsx new file mode 100644 index 0000000000000..3acf6a269fb38 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.test.tsx @@ -0,0 +1,155 @@ +/* + * 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 { act, fireEvent } from '@testing-library/react'; + +import { render } from '../../../../../lib/helper/rtl_helpers'; + +import 'jest-canvas-mock'; +import { MIME_FILTERS, WaterfallFilter } from './waterfall_filter'; +import { + FILTER_REQUESTS_LABEL, + FILTER_COLLAPSE_REQUESTS_LABEL, + FILTER_POPOVER_OPEN_LABEL, +} from '../../waterfall/components/translations'; + +describe('waterfall filter', () => { + jest.useFakeTimers(); + + it('renders correctly', () => { + const { getByLabelText, getByTitle } = render( + + ); + + fireEvent.click(getByLabelText(FILTER_POPOVER_OPEN_LABEL)); + + MIME_FILTERS.forEach((filter) => { + expect(getByTitle(filter.label)); + }); + }); + + it('filter icon changes color on active/inactive filters', () => { + const Component = () => { + const [activeFilters, setActiveFilters] = useState([]); + + return ( + + ); + }; + const { getByLabelText, getByTitle } = render(); + + fireEvent.click(getByLabelText(FILTER_POPOVER_OPEN_LABEL)); + + fireEvent.click(getByTitle('XHR')); + + expect(getByLabelText(FILTER_POPOVER_OPEN_LABEL)).toHaveAttribute( + 'class', + 'euiButtonIcon euiButtonIcon--primary' + ); + + // toggle it back to inactive + fireEvent.click(getByTitle('XHR')); + + expect(getByLabelText(FILTER_POPOVER_OPEN_LABEL)).toHaveAttribute( + 'class', + 'euiButtonIcon euiButtonIcon--text' + ); + }); + + it('search input is working properly', () => { + const setQuery = jest.fn(); + + const Component = () => { + return ( + + ); + }; + const { getByLabelText } = render(); + + const testText = 'js'; + + fireEvent.change(getByLabelText(FILTER_REQUESTS_LABEL), { target: { value: testText } }); + + // inout has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + expect(setQuery).toHaveBeenCalledWith(testText); + }); + + it('resets checkbox when filters are removed', () => { + const Component = () => { + const [onlyHighlighted, setOnlyHighlighted] = useState(false); + const [query, setQuery] = useState(''); + const [activeFilters, setActiveFilters] = useState([]); + return ( + + ); + }; + const { getByLabelText, getByTitle } = render(); + const input = getByLabelText(FILTER_REQUESTS_LABEL); + // apply filters + const testText = 'js'; + fireEvent.change(input, { target: { value: testText } }); + fireEvent.click(getByLabelText(FILTER_POPOVER_OPEN_LABEL)); + const filterGroupButton = getByTitle('XHR'); + fireEvent.click(filterGroupButton); + + // input has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + const collapseCheckbox = getByLabelText(FILTER_COLLAPSE_REQUESTS_LABEL) as HTMLInputElement; + expect(collapseCheckbox).not.toBeDisabled(); + fireEvent.click(collapseCheckbox); + expect(collapseCheckbox).toBeChecked(); + + // remove filters + fireEvent.change(input, { target: { value: '' } }); + fireEvent.click(filterGroupButton); + + // input has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + // expect the checkbox to reset to disabled and unchecked + expect(collapseCheckbox).not.toBeChecked(); + expect(collapseCheckbox).toBeDisabled(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.tsx new file mode 100644 index 0000000000000..42c2df4553b4c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.tsx @@ -0,0 +1,188 @@ +/* + * 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, { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import { + EuiButtonIcon, + EuiCheckbox, + EuiFieldSearch, + EuiFilterButton, + EuiFilterGroup, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiSpacer, +} from '@elastic/eui'; +import useDebounce from 'react-use/lib/useDebounce'; +import { + FILTER_REQUESTS_LABEL, + FILTER_SCREENREADER_LABEL, + FILTER_REMOVE_SCREENREADER_LABEL, + FILTER_POPOVER_OPEN_LABEL, + FILTER_COLLAPSE_REQUESTS_LABEL, +} from '../../waterfall/components/translations'; +import { MimeType, FriendlyMimetypeLabels } from './types'; +import { METRIC_TYPE, useUiTracker } from '../../../../../../../observability/public'; + +interface Props { + query: string; + activeFilters: string[]; + setActiveFilters: Dispatch>; + setQuery: (val: string) => void; + onlyHighlighted: boolean; + setOnlyHighlighted: (val: boolean) => void; +} + +export const MIME_FILTERS = [ + { + label: FriendlyMimetypeLabels[MimeType.XHR], + mimeType: MimeType.XHR, + }, + { + label: FriendlyMimetypeLabels[MimeType.Html], + mimeType: MimeType.Html, + }, + { + label: FriendlyMimetypeLabels[MimeType.Script], + mimeType: MimeType.Script, + }, + { + label: FriendlyMimetypeLabels[MimeType.Stylesheet], + mimeType: MimeType.Stylesheet, + }, + { + label: FriendlyMimetypeLabels[MimeType.Font], + mimeType: MimeType.Font, + }, + { + label: FriendlyMimetypeLabels[MimeType.Media], + mimeType: MimeType.Media, + }, +]; + +export const WaterfallFilter = ({ + query, + setQuery, + activeFilters, + setActiveFilters, + onlyHighlighted, + setOnlyHighlighted, +}: Props) => { + const [value, setValue] = useState(query); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const trackMetric = useUiTracker({ app: 'uptime' }); + + const toggleFilters = (val: string) => { + setActiveFilters((prevState) => + prevState.includes(val) ? prevState.filter((filter) => filter !== val) : [...prevState, val] + ); + }; + useDebounce( + () => { + setQuery(value); + }, + 250, + [value] + ); + + /* reset checkbox when there is no query or active filters + * this prevents the checkbox from being checked in a disabled state */ + useEffect(() => { + if (!(query || activeFilters.length > 0)) { + setOnlyHighlighted(false); + } + }, [activeFilters.length, setOnlyHighlighted, query]); + + // indicates use of the query input box + useEffect(() => { + if (query) { + trackMetric({ metric: 'waterfall_filter_input_changed', metricType: METRIC_TYPE.CLICK }); + } + }, [query, trackMetric]); + + // indicates the collapse to show only highlighted checkbox has been clicked + useEffect(() => { + if (onlyHighlighted) { + trackMetric({ + metric: 'waterfall_filter_collapse_checked', + metricType: METRIC_TYPE.CLICK, + }); + } + }, [onlyHighlighted, trackMetric]); + + // indicates filters have been applied or changed + useEffect(() => { + if (activeFilters.length > 0) { + trackMetric({ + metric: `waterfall_filters_applied_changed`, + metricType: METRIC_TYPE.CLICK, + }); + } + }, [activeFilters, trackMetric]); + + return ( + + + { + setValue(evt.target.value); + }} + value={value} + /> + + + setIsPopoverOpen((prevState) => !prevState)} + color={activeFilters.length > 0 ? 'primary' : 'text'} + isSelected={activeFilters.length > 0} + /> + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + anchorPosition="rightCenter" + > + + {MIME_FILTERS.map(({ label, mimeType }) => ( + toggleFilters(mimeType)} + key={label} + withNext={true} + aria-label={`${ + activeFilters.includes(mimeType) + ? FILTER_REMOVE_SCREENREADER_LABEL + : FILTER_SCREENREADER_LABEL + } ${label}`} + > + {label} + + ))} + + + 0)} + id="onlyHighlighted" + label={FILTER_COLLAPSE_REQUESTS_LABEL} + checked={onlyHighlighted} + onChange={(e) => { + setOnlyHighlighted(e.target.checked); + }} + /> + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx new file mode 100644 index 0000000000000..25b577ef9403a --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx @@ -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 React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import { SidebarItem } from '../waterfall/types'; +import { MiddleTruncatedText } from '../../waterfall'; +import { SideBarItemHighlighter } from '../../waterfall/components/styles'; +import { SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL } from '../../waterfall/components/translations'; + +interface SidebarItemProps { + item: SidebarItem; + renderFilterScreenReaderText?: boolean; +} + +export const WaterfallSidebarItem = ({ item, renderFilterScreenReaderText }: SidebarItemProps) => { + const { status, offsetIndex, isHighlighted } = item; + + const isErrorStatusCode = (statusCode: number) => { + const is400 = statusCode >= 400 && statusCode <= 499; + const is500 = statusCode >= 500 && statusCode <= 599; + const isSpecific300 = statusCode === 301 || statusCode === 307 || statusCode === 308; + return is400 || is500 || isSpecific300; + }; + + const text = `${offsetIndex}. ${item.url}`; + const ariaLabel = `${ + isHighlighted && renderFilterScreenReaderText + ? `${SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL} ` + : '' + }${text}`; + + return ( + + {!status || !isErrorStatusCode(status) ? ( + + ) : ( + + + + + + {status} + + + )} + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfalll_sidebar_item.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfalll_sidebar_item.test.tsx new file mode 100644 index 0000000000000..578d66a1ea3f1 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfalll_sidebar_item.test.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { SidebarItem } from '../waterfall/types'; + +import { render } from '../../../../../lib/helper/rtl_helpers'; + +import 'jest-canvas-mock'; +import { WaterfallSidebarItem } from './waterfall_sidebar_item'; +import { SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL } from '../../waterfall/components/translations'; + +describe('waterfall filter', () => { + const url = 'http://www.elastic.co'; + const offsetIndex = 1; + const item: SidebarItem = { + url, + isHighlighted: true, + offsetIndex, + }; + + it('renders sidbar item', () => { + const { getByText } = render(); + + expect(getByText(`${offsetIndex}. ${url}`)); + }); + + it('render screen reader text when renderFilterScreenReaderText is true', () => { + const { getByLabelText } = render( + + ); + + expect( + getByLabelText(`${SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL} ${offsetIndex}. ${url}`) + ).toBeInTheDocument(); + }); + + it('does not render screen reader text when renderFilterScreenReaderText is false', () => { + const { queryByLabelText } = render( + + ); + + expect( + queryByLabelText(`${SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL} ${offsetIndex}. ${url}`) + ).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts index 543d6004b8955..a4b75174543a8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts @@ -17,3 +17,5 @@ export const FIXED_AXIS_HEIGHT = 32; // number of items to display in canvas, since canvas can only have limited size export const CANVAS_MAX_ITEMS = 150; + +export const CHART_LEGEND_PADDING = 62; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx index 9a3d4efb63a3a..d6c1d777a40a7 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx @@ -25,15 +25,21 @@ describe('getChunks', () => { }); describe('Component', () => { - it('renders truncated text', () => { - const { getByText } = render(); + it('renders truncated text and aria label', () => { + const { getByText, getByLabelText } = render( + + ); expect(getByText(first)).toBeInTheDocument(); expect(getByText(last)).toBeInTheDocument(); + + expect(getByLabelText(longString)).toBeInTheDocument(); }); it('renders screen reader only text', () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + ); const { getByText } = within(getByTestId('middleTruncatedTextSROnly')); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx index 9c263312f78f5..ec363ed2b40a4 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx @@ -10,6 +10,11 @@ import styled from 'styled-components'; import { EuiScreenReaderOnly, EuiToolTip } from '@elastic/eui'; import { FIXED_AXIS_HEIGHT } from './constants'; +interface Props { + ariaLabel: string; + text: string; +} + const OuterContainer = styled.div` width: 100%; height: 100%; @@ -50,14 +55,14 @@ export const getChunks = (text: string) => { // Helper component for adding middle text truncation, e.g. // really-really-really-long....ompressed.js // Can be used to accomodate content in sidebar item rendering. -export const MiddleTruncatedText = ({ text }: { text: string }) => { +export const MiddleTruncatedText = ({ ariaLabel, text }: Props) => { const chunks = useMemo(() => { return getChunks(text); }, [text]); return ( <> - + {text} diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx index f46bab8c33a85..63b4d2945a51c 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx @@ -12,7 +12,11 @@ import { render } from '../../../../../lib/helper/rtl_helpers'; describe('NetworkRequestsTotal', () => { it('message in case total is greater than fetched', () => { const { getByText, getByLabelText } = render( - + ); expect(getByText('First 1000/1100 network requests')).toBeInTheDocument(); @@ -21,9 +25,52 @@ describe('NetworkRequestsTotal', () => { it('message in case total is equal to fetched requests', () => { const { getByText } = render( - + ); expect(getByText('500 network requests')).toBeInTheDocument(); }); + + it('does not show highlighted item message when showHighlightedNetworkEvents is false', () => { + const { queryByText } = render( + + ); + + expect(queryByText(/match the filter/)).not.toBeInTheDocument(); + }); + + it('does not show highlighted item message when highlightedNetworkEvents is less than 0', () => { + const { queryByText } = render( + + ); + + expect(queryByText(/match the filter/)).not.toBeInTheDocument(); + }); + + it('show highlighted item message when highlightedNetworkEvents is greater than 0 and showHighlightedNetworkEvents is true', () => { + const { getByText } = render( + + ); + + expect(getByText(/\(20 match the filter\)/)).toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx index fce86c6b5c29d..5ccd60b0ce7a8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { EuiIconTip } from '@elastic/eui'; import { NetworkRequestsTotalStyle } from './styles'; @@ -13,24 +14,44 @@ import { NetworkRequestsTotalStyle } from './styles'; interface Props { totalNetworkRequests: number; fetchedNetworkRequests: number; + highlightedNetworkRequests: number; + showHighlightedNetworkRequests?: boolean; } -export const NetworkRequestsTotal = ({ totalNetworkRequests, fetchedNetworkRequests }: Props) => { +export const NetworkRequestsTotal = ({ + totalNetworkRequests, + fetchedNetworkRequests, + highlightedNetworkRequests, + showHighlightedNetworkRequests, +}: Props) => { return ( - {i18n.translate('xpack.uptime.synthetics.waterfall.requestsTotalMessage', { - defaultMessage: '{numNetworkRequests} network requests', - values: { + fetchedNetworkRequests - ? i18n.translate('xpack.uptime.synthetics.waterfall.requestsTotalMessage.first', { - defaultMessage: 'First {count}', - values: { count: `${fetchedNetworkRequests}/${totalNetworkRequests}` }, - }) - : totalNetworkRequests, - }, - })} + totalNetworkRequests > fetchedNetworkRequests ? ( + + ) : ( + totalNetworkRequests + ), + }} + />{' '} + {showHighlightedNetworkRequests && highlightedNetworkRequests >= 0 && ( + + )} {totalNetworkRequests > fetchedNetworkRequests && ( = ({ items, render }) => { return ( - + - {items.map((item, index) => { - return ( - - {render(item, index)} - - ); - })} + {items.map((item) => ( + + {render(item)} + + ))} diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts index 333acd6e043df..9177902f8a613 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts @@ -5,19 +5,18 @@ * 2.0. */ -import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText, EuiPanelProps } from '@elastic/eui'; import { rgba } from 'polished'; -import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; +import { FunctionComponent } from 'react'; +import { StyledComponent } from 'styled-components'; +import { euiStyled, EuiTheme } from '../../../../../../../../../src/plugins/kibana_react/common'; import { FIXED_AXIS_HEIGHT } from './constants'; interface WaterfallChartOuterContainerProps { height?: string; } -export const WaterfallChartOuterContainer = euiStyled.div` - height: ${(props) => (props.height ? `${props.height}` : 'auto')}; - overflow-y: ${(props) => (props.height ? 'scroll' : 'visible')}; - overflow-x: hidden; +const StyledScrollDiv = euiStyled.div` &::-webkit-scrollbar { height: ${({ theme }) => theme.eui.euiScrollBar}; width: ${({ theme }) => theme.eui.euiScrollBar}; @@ -33,22 +32,50 @@ export const WaterfallChartOuterContainer = euiStyled.div` + height: ${(props) => (props.height ? `${props.height}` : 'auto')}; + overflow-y: ${(props) => (props.height ? 'scroll' : 'visible')}; + overflow-x: hidden; +`; + +export const WaterfallChartFixedTopContainer = euiStyled(StyledScrollDiv)` position: sticky; top: 0; z-index: ${(props) => props.theme.eui.euiZLevel4}; - border-bottom: ${(props) => `1px solid ${props.theme.eui.euiColorLightShade}`}; + overflow-y: scroll; + overflow-x: hidden; `; -export const WaterfallChartFixedTopContainerSidebarCover = euiStyled(EuiPanel)` +export const WaterfallChartAxisOnlyContainer = euiStyled(EuiFlexItem)` + margin-left: -22px; +`; + +export const WaterfallChartTopContainer = euiStyled(EuiFlexGroup)` +`; + +export const WaterfallChartFixedTopContainerSidebarCover: StyledComponent< + FunctionComponent, + EuiTheme +> = euiStyled(EuiPanel)` height: 100%; border-radius: 0 !important; border: none; `; // NOTE: border-radius !important is here as the "border" prop isn't working +export const WaterfallChartFilterContainer = euiStyled.div` + && { + padding: 16px; + z-index: ${(props) => props.theme.eui.euiZLevel5}; + border-bottom: 0.3px solid ${(props) => props.theme.eui.euiColorLightShade}; + } +`; // NOTE: border-radius !important is here as the "border" prop isn't working + export const WaterfallChartFixedAxisContainer = euiStyled.div` height: ${FIXED_AXIS_HEIGHT}px; z-index: ${(props) => props.theme.eui.euiZLevel4}; + height: 100%; `; interface WaterfallChartSidebarContainer { @@ -60,7 +87,10 @@ export const WaterfallChartSidebarContainer = euiStyled.div, + EuiTheme +> = euiStyled(EuiPanel)` border: 0; height: 100%; `; @@ -74,6 +104,12 @@ export const WaterfallChartSidebarFlexItem = euiStyled(EuiFlexItem)` min-width: 0; padding-left: ${(props) => props.theme.eui.paddingSizes.m}; padding-right: ${(props) => props.theme.eui.paddingSizes.m}; + z-index: ${(props) => props.theme.eui.euiZLevel4}; +`; + +export const SideBarItemHighlighter = euiStyled.span<{ isHighlighted: boolean }>` + opacity: ${(props) => (props.isHighlighted ? 1 : 0.4)}; + height: 100%; `; interface WaterfallChartChartContainer { @@ -106,6 +142,12 @@ export const WaterfallChartTooltip = euiStyled.div` `; export const NetworkRequestsTotalStyle = euiStyled(EuiText)` - line-height: ${FIXED_AXIS_HEIGHT}px; - margin-left: ${(props) => props.theme.eui.paddingSizes.m} + line-height: 28px; + padding: 0 ${(props) => props.theme.eui.paddingSizes.m}; + border-bottom: 0.3px solid ${(props) => props.theme.eui.euiColorLightShade}; + z-index: ${(props) => props.theme.eui.euiZLevel5}; +`; + +export const RelativeContainer = euiStyled.div` + position: relative; `; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/translations.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/translations.ts new file mode 100644 index 0000000000000..b63ffacaadd2e --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/translations.ts @@ -0,0 +1,50 @@ +/* + * 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'; + +export const FILTER_REQUESTS_LABEL = i18n.translate( + 'xpack.uptime.synthetics.waterfall.searchBox.placeholder', + { + defaultMessage: 'Filter network requests', + } +); + +export const FILTER_SCREENREADER_LABEL = i18n.translate( + 'xpack.uptime.synthetics.waterfall.filterGroup.filterScreenreaderLabel', + { + defaultMessage: 'Filter by', + } +); + +export const FILTER_REMOVE_SCREENREADER_LABEL = i18n.translate( + 'xpack.uptime.synthetics.waterfall.filterGroup.removeFilterScreenReaderLabel', + { + defaultMessage: 'Remove filter by', + } +); + +export const FILTER_POPOVER_OPEN_LABEL = i18n.translate( + 'xpack.uptime.pingList.synthetics.waterfall.filters.popover', + { + defaultMessage: 'Click to open waterfall filters', + } +); + +export const FILTER_COLLAPSE_REQUESTS_LABEL = i18n.translate( + 'xpack.uptime.pingList.synthetics.waterfall.filters.collapseRequestsLabel', + { + defaultMessage: 'Collapse to only show matching requests', + } +); + +export const SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL = i18n.translate( + 'xpack.uptime.synthetics.waterfall.sidebar.filterMatchesScreenReaderLabel', + { + defaultMessage: 'Resource matches filter', + } +); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.test.tsx index 1ce46fc0d6e7b..a963fb1e2939c 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.test.tsx @@ -10,9 +10,14 @@ import { renderHook } from '@testing-library/react-hooks'; import { IWaterfallContext } from '../context/waterfall_chart'; import { CANVAS_MAX_ITEMS } from './constants'; -const generateTestData = (): IWaterfallContext['data'] => { +const generateTestData = ( + { + xMultiplier, + }: { + xMultiplier: number; + } = { xMultiplier: 1 } +): IWaterfallContext['data'] => { const numberOfItems = 1000; - const data: IWaterfallContext['data'] = []; const testItem = { x: 0, @@ -29,11 +34,11 @@ const generateTestData = (): IWaterfallContext['data'] => { data.push( { ...testItem, - x: i, + x: xMultiplier * i, }, { ...testItem, - x: i, + x: xMultiplier * i, y0: 7, y: 25, } @@ -44,7 +49,7 @@ const generateTestData = (): IWaterfallContext['data'] => { }; describe('useBarChartsHooks', () => { - it('returns result as expected', () => { + it('returns result as expected for non filtered data', () => { const { result, rerender } = renderHook((props) => useBarCharts(props), { initialProps: { data: [] as IWaterfallContext['data'] }, }); @@ -70,4 +75,35 @@ describe('useBarChartsHooks', () => { expect(lastChartItems[0].x).toBe(CANVAS_MAX_ITEMS * 4); expect(lastChartItems[lastChartItems.length - 1].x).toBe(CANVAS_MAX_ITEMS * 5 - 1); }); + + it('returns result as expected for filtered data', () => { + /* multiply x values to simulate filtered data, where x values can have gaps in the + * sequential order */ + const xMultiplier = 2; + const { result, rerender } = renderHook((props) => useBarCharts(props), { + initialProps: { data: [] as IWaterfallContext['data'] }, + }); + + expect(result.current).toHaveLength(0); + const newData = generateTestData({ xMultiplier }); + + rerender({ data: newData }); + + // Thousands items will result in 7 Canvas + expect(result.current.length).toBe(7); + + const firstChartItems = result.current[0]; + const lastChartItems = result.current[4]; + + // first chart items last item should be x 149, since we only display 150 items + expect(firstChartItems[firstChartItems.length - 1].x).toBe( + (CANVAS_MAX_ITEMS - 1) * xMultiplier + ); + + // since here are 5 charts, last chart first item should be x 600 + expect(lastChartItems[0].x).toBe(CANVAS_MAX_ITEMS * 4 * xMultiplier); + expect(lastChartItems[lastChartItems.length - 1].x).toBe( + (CANVAS_MAX_ITEMS * 5 - 1) * xMultiplier + ); + }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.ts index 79fd437039afe..2baf895504911 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.ts @@ -13,27 +13,36 @@ export interface UseBarHookProps { data: IWaterfallContext['data']; } -export const useBarCharts = ({ data = [] }: UseBarHookProps) => { +export const useBarCharts = ({ data }: UseBarHookProps) => { const [charts, setCharts] = useState>([]); useEffect(() => { - if (data.length > 0) { - let chartIndex = 0; - - const chartsN: Array = []; + const chartsN: Array = []; + if (data?.length > 0) { + let chartIndex = 0; + /* We want at most CANVAS_MAX_ITEMS **RESOURCES** per array. + * Resources !== individual timing items, but are comprised of many individual timing + * items. The X value of each item can be used as an id for the resource. + * We must keep track of the number of unique resources added to the each array. */ + const uniqueResources = new Set(); + let lastIndex: number; data.forEach((item) => { - // Subtract 1 to account for x value starting from 0 - if (item.x === CANVAS_MAX_ITEMS * chartIndex && !chartsN[item.x / CANVAS_MAX_ITEMS]) { - chartsN.push([item]); + if (uniqueResources.size === CANVAS_MAX_ITEMS && item.x > lastIndex) { chartIndex++; + uniqueResources.clear(); + } + uniqueResources.add(item.x); + lastIndex = item.x; + if (!chartsN[chartIndex]) { + chartsN.push([item]); return; } - chartsN[chartIndex - 1].push(item); + chartsN[chartIndex].push(item); }); - - setCharts(chartsN); } + + setCharts(chartsN); }, [data]); return charts; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall.test.tsx index 7c9051e8f6acf..528d749f576fc 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall.test.tsx @@ -6,64 +6,38 @@ */ import React from 'react'; -import { of } from 'rxjs'; -import { MountWithReduxProvider, mountWithRouter } from '../../../../../lib'; -import { KibanaContextProvider } from '../../../../../../../../../src/plugins/kibana_react/public'; import { WaterfallChart } from './waterfall_chart'; -import { - renderLegendItem, - renderSidebarItem, -} from '../../step_detail/waterfall/waterfall_chart_wrapper'; -import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common'; -import { WaterfallChartOuterContainer } from './styles'; +import { renderLegendItem } from '../../step_detail/waterfall/waterfall_chart_wrapper'; +import { render } from '../../../../../lib/helper/rtl_helpers'; + +import 'jest-canvas-mock'; describe('waterfall', () => { it('sets the correct height in case of full height', () => { - const core = mockCore(); - const Component = () => { return ( - `${Number(d).toFixed(0)} ms`} - domain={{ - max: 3371, - min: 0, - }} - barStyleAccessor={(datum) => { - return datum.datum.config.colour; - }} - renderSidebarItem={renderSidebarItem} - renderLegendItem={renderLegendItem} - fullHeight={true} - /> +

+ `${Number(d).toFixed(0)} ms`} + domain={{ + max: 3371, + min: 0, + }} + barStyleAccessor={(datum) => { + return datum.datum.config.colour; + }} + renderSidebarItem={undefined} + renderLegendItem={renderLegendItem} + fullHeight={true} + /> +
); }; - const component = mountWithRouter( - - - - - - - - ); + const { getByTestId } = render(); - const chartWrapper = component.find(WaterfallChartOuterContainer); + const chartWrapper = getByTestId('waterfallOuterContainer'); - expect(chartWrapper.get(0).props.height).toBe('calc(100vh - 0px)'); + expect(chartWrapper).toHaveStyleRule('height', 'calc(100vh - 62px)'); }); }); - -const mockCore: () => any = () => { - return { - application: { - getUrlForApp: () => '/app/uptime', - navigateToUrl: jest.fn(), - }, - uiSettings: { - get: (key: string) => 'MMM D, YYYY @ HH:mm:ss.SSS', - get$: (key: string) => of('MMM D, YYYY @ HH:mm:ss.SSS'), - }, - }; -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx new file mode 100644 index 0000000000000..df00df147fc6c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + Axis, + BarSeries, + BarStyleAccessor, + Chart, + DomainRange, + Position, + ScaleType, + Settings, + TickFormatter, + TooltipInfo, +} from '@elastic/charts'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { BAR_HEIGHT } from './constants'; +import { useChartTheme } from '../../../../../hooks/use_chart_theme'; +import { WaterfallChartChartContainer, WaterfallChartTooltip } from './styles'; +import { useWaterfallContext, WaterfallData } from '..'; + +const getChartHeight = (data: WaterfallData): number => { + // We get the last item x(number of bars) and adds 1 to cater for 0 index + const noOfXBars = new Set(data.map((item) => item.x)).size; + + return noOfXBars * BAR_HEIGHT; +}; + +const Tooltip = (tooltipInfo: TooltipInfo) => { + const { data, renderTooltipItem } = useWaterfallContext(); + const relevantItems = data.filter((item) => { + return ( + item.x === tooltipInfo.header?.value && item.config.showTooltip && item.config.tooltipProps + ); + }); + return relevantItems.length ? ( + + + {relevantItems.map((item, index) => { + return ( + {renderTooltipItem(item.config.tooltipProps)} + ); + })} + + + ) : null; +}; + +interface Props { + index: number; + chartData: WaterfallData; + tickFormat: TickFormatter; + domain: DomainRange; + barStyleAccessor: BarStyleAccessor; +} + +export const WaterfallBarChart = ({ + chartData, + tickFormat, + domain, + barStyleAccessor, + index, +}: Props) => { + const theme = useChartTheme(); + + return ( + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx index 8f831d0629b25..e0e5165b41e49 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx @@ -5,62 +5,30 @@ * 2.0. */ -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { - Axis, - BarSeries, - Chart, - Position, - ScaleType, - Settings, - TickFormatter, - DomainRange, - BarStyleAccessor, - TooltipInfo, - TooltipType, -} from '@elastic/charts'; -import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; -// NOTE: The WaterfallChart has a hard requirement that consumers / solutions are making use of KibanaReactContext, and useKibana etc -// can therefore be accessed. -import { useUiSetting$ } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { TickFormatter, DomainRange, BarStyleAccessor } from '@elastic/charts'; + import { useWaterfallContext } from '../context/waterfall_chart'; import { WaterfallChartOuterContainer, WaterfallChartFixedTopContainer, WaterfallChartFixedTopContainerSidebarCover, - WaterfallChartFixedAxisContainer, - WaterfallChartChartContainer, - WaterfallChartTooltip, + WaterfallChartTopContainer, + RelativeContainer, + WaterfallChartFilterContainer, + WaterfallChartAxisOnlyContainer, } from './styles'; -import { WaterfallData } from '../types'; -import { BAR_HEIGHT, CANVAS_MAX_ITEMS, MAIN_GROW_SIZE, SIDEBAR_GROW_SIZE } from './constants'; +import { CHART_LEGEND_PADDING, MAIN_GROW_SIZE, SIDEBAR_GROW_SIZE } from './constants'; import { Sidebar } from './sidebar'; import { Legend } from './legend'; import { useBarCharts } from './use_bar_charts'; +import { WaterfallBarChart } from './waterfall_bar_chart'; +import { WaterfallChartFixedAxis } from './waterfall_chart_fixed_axis'; import { NetworkRequestsTotal } from './network_requests_total'; -const Tooltip = (tooltipInfo: TooltipInfo) => { - const { data, renderTooltipItem } = useWaterfallContext(); - const relevantItems = data.filter((item) => { - return ( - item.x === tooltipInfo.header?.value && item.config.showTooltip && item.config.tooltipProps - ); - }); - return relevantItems.length ? ( - - - {relevantItems.map((item, index) => { - return ( - {renderTooltipItem(item.config.tooltipProps)} - ); - })} - - - ) : null; -}; - -export type RenderItem = (item: I, index: number) => JSX.Element; +export type RenderItem = (item: I, index?: number) => JSX.Element; +export type RenderFilter = () => JSX.Element; export interface WaterfallChartProps { tickFormat: TickFormatter; @@ -68,159 +36,100 @@ export interface WaterfallChartProps { barStyleAccessor: BarStyleAccessor; renderSidebarItem?: RenderItem; renderLegendItem?: RenderItem; + renderFilter?: RenderFilter; maxHeight?: string; fullHeight?: boolean; } -const getChartHeight = (data: WaterfallData, ind: number): number => { - // We get the last item x(number of bars) and adds 1 to cater for 0 index - return (data[data.length - 1]?.x + 1 - ind * CANVAS_MAX_ITEMS) * BAR_HEIGHT; -}; - export const WaterfallChart = ({ tickFormat, domain, barStyleAccessor, renderSidebarItem, renderLegendItem, + renderFilter, maxHeight = '800px', fullHeight = false, }: WaterfallChartProps) => { const { data, + showOnlyHighlightedNetworkRequests, sidebarItems, legendItems, totalNetworkRequests, + highlightedNetworkRequests, fetchedNetworkRequests, } = useWaterfallContext(); - const [darkMode] = useUiSetting$('theme:darkMode'); - - const theme = useMemo(() => { - return darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; - }, [darkMode]); - const chartWrapperDivRef = useRef(null); const [height, setHeight] = useState(maxHeight); - const shouldRenderSidebar = !!(sidebarItems && sidebarItems.length > 0 && renderSidebarItem); + const shouldRenderSidebar = !!(sidebarItems && renderSidebarItem); const shouldRenderLegend = !!(legendItems && legendItems.length > 0 && renderLegendItem); useEffect(() => { if (fullHeight && chartWrapperDivRef.current) { const chartOffset = chartWrapperDivRef.current.getBoundingClientRect().top; - setHeight(`calc(100vh - ${chartOffset}px)`); + setHeight(`calc(100vh - ${chartOffset + CHART_LEGEND_PADDING}px)`); } }, [chartWrapperDivRef, fullHeight]); const chartsToDisplay = useBarCharts({ data }); return ( - - <> - - - {shouldRenderSidebar && ( - - - - - - )} - - - - - - - - - - + + + + {shouldRenderSidebar && ( + + + + {renderFilter && ( + {renderFilter()} + )} - - - + )} + + + + + + + + {shouldRenderSidebar && } - + + {chartsToDisplay.map((chartData, ind) => ( - - - - - - - - - + chartData={chartData} + domain={domain} + barStyleAccessor={barStyleAccessor} + tickFormat={tickFormat} + /> ))} - + - {shouldRenderLegend && } - - + + {shouldRenderLegend && } + ); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx new file mode 100644 index 0000000000000..3a7ab421b6277 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx @@ -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 React from 'react'; +import { + Axis, + BarSeries, + BarStyleAccessor, + Chart, + DomainRange, + Position, + ScaleType, + Settings, + TickFormatter, + TooltipType, +} from '@elastic/charts'; +import { useChartTheme } from '../../../../../hooks/use_chart_theme'; +import { WaterfallChartFixedAxisContainer } from './styles'; + +interface Props { + tickFormat: TickFormatter; + domain: DomainRange; + barStyleAccessor: BarStyleAccessor; +} + +export const WaterfallChartFixedAxis = ({ tickFormat, domain, barStyleAccessor }: Props) => { + const theme = useChartTheme(); + + return ( + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx index 68d24514a37d3..9e87d69ce38a8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx @@ -7,12 +7,15 @@ import React, { createContext, useContext, Context } from 'react'; import { WaterfallData, WaterfallDataEntry } from '../types'; +import { SidebarItems } from '../../step_detail/waterfall/types'; export interface IWaterfallContext { totalNetworkRequests: number; + highlightedNetworkRequests: number; fetchedNetworkRequests: number; data: WaterfallData; - sidebarItems?: unknown[]; + showOnlyHighlightedNetworkRequests: boolean; + sidebarItems?: SidebarItems; legendItems?: unknown[]; renderTooltipItem: ( item: WaterfallDataEntry['config']['tooltipProps'], @@ -24,8 +27,10 @@ export const WaterfallContext = createContext>({}); interface ProviderProps { totalNetworkRequests: number; + highlightedNetworkRequests: number; fetchedNetworkRequests: number; data: IWaterfallContext['data']; + showOnlyHighlightedNetworkRequests: IWaterfallContext['showOnlyHighlightedNetworkRequests']; sidebarItems?: IWaterfallContext['sidebarItems']; legendItems?: IWaterfallContext['legendItems']; renderTooltipItem: IWaterfallContext['renderTooltipItem']; @@ -34,20 +39,24 @@ interface ProviderProps { export const WaterfallProvider: React.FC = ({ children, data, + showOnlyHighlightedNetworkRequests, sidebarItems, legendItems, renderTooltipItem, totalNetworkRequests, + highlightedNetworkRequests, fetchedNetworkRequests, }) => { return ( diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx index ecc6231ba05fd..9ee6dc749b9eb 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx @@ -12,7 +12,7 @@ import { IntegrationGroup } from './integration_group'; import { MonitorSummary } from '../../../../../../common/runtime_types'; import { toggleIntegrationsPopover, PopoverState } from '../../../../../state/actions'; -interface ActionsPopoverProps { +export interface ActionsPopoverProps { summary: MonitorSummary; popoverState: PopoverState | null; togglePopoverIsVisible: typeof toggleIntegrationsPopover; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/data.json b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/data.json index 1bbdcd4a30078..905e982681dee 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/data.json +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/data.json @@ -261,7 +261,25 @@ }, "state": { "agent": null, - "checks": , + "checks": [ + { + "agent": { "id": "8f9a37fb-573a-4fdc-9895-440a5b39c250", "__typename": "Agent" }, + "container": null, + "kubernetes": null, + "monitor": { + "ip": "127.0.0.1", + "name": "localhost", + "status": "up", + "__typename": "CheckMonitor" + }, + "observer": { + "geo": { "name": null, "location": null, "__typename": "CheckGeo" }, + "__typename": "CheckObserver" + }, + "timestamp": "1570538246143", + "__typename": "Check" + } + ], "geo": null, "observer": { "geo": { "name": [], "location": null, "__typename": "StateGeo" }, diff --git a/x-pack/plugins/uptime/public/hooks/use_chart_theme.ts b/x-pack/plugins/uptime/public/hooks/use_chart_theme.ts new file mode 100644 index 0000000000000..f9231abaa75a8 --- /dev/null +++ b/x-pack/plugins/uptime/public/hooks/use_chart_theme.ts @@ -0,0 +1,20 @@ +/* + * 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 { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; +import { useMemo } from 'react'; +import { useUiSetting$ } from '../../../../../src/plugins/kibana_react/public'; + +export const useChartTheme = () => { + const [darkMode] = useUiSetting$('theme:darkMode'); + + const theme = useMemo(() => { + return darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; + }, [darkMode]); + + return theme; +}; diff --git a/x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx b/x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx index 9656c63274a13..4c81247fb2cf1 100644 --- a/x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx @@ -8,10 +8,17 @@ import React, { ReactElement } from 'react'; import { Router } from 'react-router-dom'; import { MemoryHistory } from 'history/createMemoryHistory'; -import { createMemoryHistory } from 'history'; +import { createMemoryHistory, History } from 'history'; import { mountWithIntl, renderWithIntl, shallowWithIntl } from '@kbn/test/jest'; import { MountWithReduxProvider } from './helper_with_redux'; import { AppState } from '../../state'; +import { mockState } from '../__mocks__/uptime_store.mock'; +import { KibanaProviderOptions, MockRouter } from './rtl_helpers'; + +interface RenderRouterOptions extends KibanaProviderOptions { + history?: History; + state?: Partial; +} const helperWithRouter: ( helper: (node: ReactElement) => R, @@ -67,3 +74,39 @@ export const mountWithRouterRedux = ( options?.storeState ); }; + +/* Custom enzyme render */ +export function render( + ui: ReactElement, + { history, core, kibanaProps, state }: RenderRouterOptions = {} +) { + const testState: AppState = { + ...mockState, + ...state, + }; + return renderWithIntl( + + + {ui} + + + ); +} + +/* Custom enzyme render */ +export function mount( + ui: ReactElement, + { history, core, kibanaProps, state }: RenderRouterOptions = {} +) { + const testState: AppState = { + ...mockState, + ...state, + }; + return mountWithIntl( + + + {ui} + + + ); +} diff --git a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx index abc0451bf8efa..e02a2c6f9832f 100644 --- a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx @@ -6,6 +6,7 @@ */ import React, { ReactElement } from 'react'; +import { of } from 'rxjs'; import { render as reactTestLibRender, RenderOptions } from '@testing-library/react'; import { Router } from 'react-router-dom'; import { createMemoryHistory, History } from 'history'; @@ -26,7 +27,7 @@ interface KibanaProps { services?: KibanaServices; } -interface KibanaProviderOptions { +export interface KibanaProviderOptions { core?: Partial & ExtraCore; kibanaProps?: KibanaProps; } @@ -54,6 +55,11 @@ const mockCore: () => any = () => { getUrlForApp: () => '/app/uptime', navigateToUrl: jest.fn(), }, + uiSettings: { + get: (key: string) => 'MMM D, YYYY @ HH:mm:ss.SSS', + get$: (key: string) => of('MMM D, YYYY @ HH:mm:ss.SSS'), + }, + usageCollection: { reportUiCounter: () => {} }, }; return core; diff --git a/x-pack/plugins/uptime/public/state/alerts/alerts.ts b/x-pack/plugins/uptime/public/state/alerts/alerts.ts index 4b48b157c3deb..f328bd5b9a5a7 100644 --- a/x-pack/plugins/uptime/public/state/alerts/alerts.ts +++ b/x-pack/plugins/uptime/public/state/alerts/alerts.ts @@ -53,7 +53,7 @@ export const deleteAnomalyAlertAction = createAsyncAction<{ alertId: string }, a 'DELETE ANOMALY ALERT' ); -interface AlertState { +export interface AlertState { connectors: AsyncInitState; newAlert: AsyncInitState>; alerts: AsyncInitState; diff --git a/x-pack/plugins/uptime/public/state/certificates/certificates.ts b/x-pack/plugins/uptime/public/state/certificates/certificates.ts index d6d48f2ab7007..ca2d5e7a17a46 100644 --- a/x-pack/plugins/uptime/public/state/certificates/certificates.ts +++ b/x-pack/plugins/uptime/public/state/certificates/certificates.ts @@ -19,7 +19,7 @@ export const getCertificatesAction = createAsyncAction; } diff --git a/x-pack/plugins/uptime/public/state/index.ts b/x-pack/plugins/uptime/public/state/index.ts index fa15e77f7fcc4..61b1a5f9d9527 100644 --- a/x-pack/plugins/uptime/public/state/index.ts +++ b/x-pack/plugins/uptime/public/state/index.ts @@ -5,17 +5,16 @@ * 2.0. */ -import { compose, createStore, applyMiddleware } from 'redux'; +import { createStore, applyMiddleware } from 'redux'; +import { composeWithDevTools } from 'redux-devtools-extension'; import createSagaMiddleware from 'redux-saga'; import { rootEffect } from './effects'; import { rootReducer } from './reducers'; export type AppState = ReturnType; -const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; - const sagaMW = createSagaMiddleware(); -export const store = createStore(rootReducer, composeEnhancers(applyMiddleware(sagaMW))); +export const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(sagaMW))); sagaMW.run(rootEffect); diff --git a/x-pack/plugins/uptime/public/state/reducers/journey.ts b/x-pack/plugins/uptime/public/state/reducers/journey.ts index 273523f4592d6..361454e1b3fa1 100644 --- a/x-pack/plugins/uptime/public/state/reducers/journey.ts +++ b/x-pack/plugins/uptime/public/state/reducers/journey.ts @@ -24,7 +24,7 @@ export interface JourneyState { error?: Error; } -interface JourneyKVP { +export interface JourneyKVP { [checkGroup: string]: JourneyState; } diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts index 6310b79206a88..0c9f9dd849341 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts @@ -10,7 +10,7 @@ import moment from 'moment'; import { schema } from '@kbn/config-schema'; import { ActionGroupIdsOf } from '../../../../alerts/common'; import { updateState } from './common'; -import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants/alerts'; +import { DURATION_ANOMALY } from '../../../common/constants/alerts'; import { commonStateTranslations, durationAnomalyTranslations } from './translations'; import { AnomaliesTableRecord } from '../../../../ml/common/types/anomalies'; import { getSeverityType } from '../../../../ml/common/util/anomaly_utils'; @@ -21,7 +21,6 @@ import { getMLJobId } from '../../../common/lib'; import { getLatestMonitor } from '../requests/get_latest_monitor'; import { uptimeAlertWrapper } from './uptime_alert_wrapper'; -const { DURATION_ANOMALY } = ACTION_GROUP_DEFINITIONS; export type ActionGroupIds = ActionGroupIdsOf; export const getAnomalySummary = (anomaly: AnomaliesTableRecord, monitorInfo: Ping) => { diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index cc1cb3a4ed0be..cee20d113c256 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -17,7 +17,7 @@ import { Ping, GetMonitorAvailabilityParams, } from '../../../common/runtime_types'; -import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants/alerts'; +import { MONITOR_STATUS } from '../../../common/constants/alerts'; import { updateState } from './common'; import { commonMonitorStateI18, commonStateTranslations, DOWN_LABEL } from './translations'; import { stringifyKueries, combineFiltersAndUserSearch } from '../../../common/lib'; @@ -29,7 +29,6 @@ import { MonitorStatusTranslations } from '../../../common/translations'; import { getUptimeIndexPattern, IndexPatternTitleAndFields } from '../requests/get_index_pattern'; import { UMServerLibs, UptimeESClient } from '../lib'; -const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS; export type ActionGroupIds = ActionGroupIdsOf; const getMonIdByLoc = (monitorId: string, location: string) => { diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls.ts b/x-pack/plugins/uptime/server/lib/alerts/tls.ts index 345d2470ed705..7bc4c36b98e8b 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/tls.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/tls.ts @@ -9,7 +9,7 @@ import moment from 'moment'; import { schema } from '@kbn/config-schema'; import { UptimeAlertTypeFactory } from './types'; import { updateState } from './common'; -import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants/alerts'; +import { TLS } from '../../../common/constants/alerts'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants'; import { Cert, CertResult } from '../../../common/runtime_types'; import { commonStateTranslations, tlsTranslations } from './translations'; @@ -17,7 +17,6 @@ import { DEFAULT_FROM, DEFAULT_TO } from '../../rest_api/certs/certs'; import { uptimeAlertWrapper } from './uptime_alert_wrapper'; import { ActionGroupIdsOf } from '../../../../alerts/common'; -const { TLS } = ACTION_GROUP_DEFINITIONS; export type ActionGroupIds = ActionGroupIdsOf; const DEFAULT_SIZE = 20; diff --git a/x-pack/plugins/uptime/server/lib/lib.ts b/x-pack/plugins/uptime/server/lib/lib.ts index 53a79815a0c0f..5ac56d14c171d 100644 --- a/x-pack/plugins/uptime/server/lib/lib.ts +++ b/x-pack/plugins/uptime/server/lib/lib.ts @@ -27,7 +27,7 @@ export interface UMServerLibs extends UMDomainLibs { framework: UMBackendFrameworkAdapter; } -interface CountResponse { +export interface CountResponse { body: { count: number; _shards: { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts index c942c3a8f69fd..e0edcc4576378 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts @@ -8,7 +8,7 @@ import { UMElasticsearchQueryFn } from '../adapters/framework'; import { SyntheticsJourneyApiResponse } from '../../../common/runtime_types'; -interface GetJourneyDetails { +export interface GetJourneyDetails { checkGroup: string; } diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_failed_steps.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_failed_steps.ts index 1abba0087cb44..9865bd95fe961 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_failed_steps.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_failed_steps.ts @@ -8,7 +8,7 @@ import { UMElasticsearchQueryFn } from '../adapters/framework'; import { Ping } from '../../../common/runtime_types'; -interface GetJourneyStepsParams { +export interface GetJourneyStepsParams { checkGroups: string[]; } diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts index ff9aec85e28bb..9cb5e1eedb6b0 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts @@ -8,7 +8,7 @@ import { UMElasticsearchQueryFn } from '../adapters/framework'; import { Ping } from '../../../common/runtime_types/ping'; -interface GetJourneyScreenshotParams { +export interface GetJourneyScreenshotParams { checkGroup: string; stepIndex: number; } diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts index d657b8b9aacf3..3055f169fc495 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts @@ -8,7 +8,7 @@ import { UMElasticsearchQueryFn } from '../adapters/framework'; import { Ping } from '../../../common/runtime_types'; -interface GetJourneyStepsParams { +export interface GetJourneyStepsParams { checkGroup: string; syntheticEventTypes?: string | string[]; } diff --git a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts index f9936c6f273ba..fa76da0025305 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts @@ -8,7 +8,7 @@ import { UMElasticsearchQueryFn } from '../adapters/framework'; import { NetworkEvent } from '../../../common/runtime_types'; -interface GetNetworkEventsParams { +export interface GetNetworkEventsParams { checkGroup: string; stepIndex: string; } diff --git a/x-pack/plugins/uptime/server/lib/requests/helper.ts b/x-pack/plugins/uptime/server/lib/requests/helper.ts index 2556d7b8fb8cd..e3969f84c8485 100644 --- a/x-pack/plugins/uptime/server/lib/requests/helper.ts +++ b/x-pack/plugins/uptime/server/lib/requests/helper.ts @@ -5,14 +5,14 @@ * 2.0. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ElasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; import { elasticsearchServiceMock, savedObjectsClientMock, } from '../../../../../../src/core/server/mocks'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ElasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; -import { createUptimeESClient } from '../lib'; +import { createUptimeESClient, UptimeESClient } from '../lib'; export interface MultiPageCriteria { after_key?: K; @@ -60,7 +60,14 @@ export const setupMockEsCompositeQuery = ( return esMock; }; -export const getUptimeESMockClient = (esClientMock?: ElasticsearchClientMock) => { +interface UptimeEsMockClient { + esClient: ElasticsearchClientMock; + uptimeEsClient: UptimeESClient; +} + +export const getUptimeESMockClient = ( + esClientMock?: ElasticsearchClientMock +): UptimeEsMockClient => { const esClient = elasticsearchServiceMock.createElasticsearchClient(); const savedObjectsClient = savedObjectsClientMock.create(); diff --git a/x-pack/plugins/uptime/tsconfig.json b/x-pack/plugins/uptime/tsconfig.json new file mode 100644 index 0000000000000..5a195f6c2df25 --- /dev/null +++ b/x-pack/plugins/uptime/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "public/components/monitor/status_details/location_map/embeddables/low_poly_layer.json", + "server/**/*", + "server/lib/requests/__fixtures__/monitor_charts_mock.json", + "../../../typings/**/*" + ], + "references": [ + { "path": "../alerts/tsconfig.json" }, + { "path": "../ml/tsconfig.json" }, + { "path": "../triggers_actions_ui/tsconfig.json" }, + { "path": "../observability/tsconfig.json" } + ] +} diff --git a/x-pack/test/accessibility/apps/upgrade_assistant.ts b/x-pack/test/accessibility/apps/upgrade_assistant.ts new file mode 100644 index 0000000000000..332a54006b0ec --- /dev/null +++ b/x-pack/test/accessibility/apps/upgrade_assistant.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 { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['upgradeAssistant', 'common']); + const a11y = getService('a11y'); + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + + describe('Upgrade Assistant Home', () => { + before(async () => { + await PageObjects.upgradeAssistant.navigateToPage(); + }); + + it('Overview Tab', async () => { + await retry.waitFor('Upgrade Assistant overview tab to be visible', async () => { + return testSubjects.exists('upgradeAssistantOverviewTabDetail'); + }); + await a11y.testAppSnapshot(); + }); + + it('Cluster Tab', async () => { + await testSubjects.click('upgradeAssistantClusterTab'); + await retry.waitFor('Upgrade Assistant Cluster tab to be visible', async () => { + return testSubjects.exists('upgradeAssistantClusterTabDetail'); + }); + await a11y.testAppSnapshot(); + }); + + it('Indices Tab', async () => { + await testSubjects.click('upgradeAssistantIndicesTab'); + await retry.waitFor('Upgrade Assistant Cluster tab to be visible', async () => { + return testSubjects.exists('upgradeAssistantIndexTabDetail'); + }); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index 94a09e3f767f6..24c46c1a1687e 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -31,6 +31,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/index_lifecycle_management'), require.resolve('./apps/ml'), require.resolve('./apps/lens'), + require.resolve('./apps/upgrade_assistant'), ], pageObjects, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index 6cc5e2eaefb94..8bd0b8a790d40 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -375,6 +375,34 @@ export default function jiraTest({ getService }: FtrProviderContext) { }); }); }); + + it('should handle failing with a simulated success when labels containing a space', async () => { + await supertest + .post(`/api/actions/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockJira.params, + subActionParams: { + incident: { + ...mockJira.params.subActionParams.incident, + issueType: '10006', + labels: ['label with spaces'], + }, + comments: [], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.incident.labels]: types that failed validation:\n - [subActionParams.incident.labels.0.0]: The label label with spaces cannot contain spaces\n - [subActionParams.incident.labels.1]: expected value to equal [null]\n- [4.subAction]: expected value to equal [issueTypes]\n- [5.subAction]: expected value to equal [fieldsByIssueType]\n- [6.subAction]: expected value to equal [issues]\n- [7.subAction]: expected value to equal [issue]', + }); + }); + }); }); describe('Execution', () => { diff --git a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts index 2705406009062..39b343a361945 100644 --- a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts +++ b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts @@ -19,97 +19,97 @@ const EXPECTED_DATA = [ category: 'base', field: '@timestamp', values: ['2019-02-10T02:39:44.107Z'], - originalValue: '2019-02-10T02:39:44.107Z', + originalValue: ['2019-02-10T02:39:44.107Z'], }, { category: '@version', field: '@version', values: ['1'], - originalValue: '1', + originalValue: ['1'], }, { category: 'agent', field: 'agent.ephemeral_id', values: ['909cd6a1-527d-41a5-9585-a7fb5386f851'], - originalValue: '909cd6a1-527d-41a5-9585-a7fb5386f851', + originalValue: ['909cd6a1-527d-41a5-9585-a7fb5386f851'], }, { category: 'agent', field: 'agent.hostname', values: ['raspberrypi'], - originalValue: 'raspberrypi', + originalValue: ['raspberrypi'], }, { category: 'agent', field: 'agent.id', values: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'], - originalValue: '4d3ea604-27e5-4ec7-ab64-44f82285d776', + originalValue: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'], }, { category: 'agent', field: 'agent.type', values: ['filebeat'], - originalValue: 'filebeat', + originalValue: ['filebeat'], }, { category: 'agent', field: 'agent.version', values: ['7.0.0'], - originalValue: '7.0.0', + originalValue: ['7.0.0'], }, { category: 'destination', field: 'destination.domain', values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], - originalValue: 's3-iad-2.cf.dash.row.aiv-cdn.net', + originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], }, { category: 'destination', field: 'destination.ip', values: ['10.100.7.196'], - originalValue: '10.100.7.196', + originalValue: ['10.100.7.196'], }, { category: 'destination', field: 'destination.port', - values: [40684], - originalValue: 40684, + values: ['40684'], + originalValue: ['40684'], }, { category: 'ecs', field: 'ecs.version', values: ['1.0.0-beta2'], - originalValue: '1.0.0-beta2', + originalValue: ['1.0.0-beta2'], }, { category: 'event', field: 'event.dataset', values: ['suricata.eve'], - originalValue: 'suricata.eve', + originalValue: ['suricata.eve'], }, { category: 'event', field: 'event.end', values: ['2019-02-10T02:39:44.107Z'], - originalValue: '2019-02-10T02:39:44.107Z', + originalValue: ['2019-02-10T02:39:44.107Z'], }, { category: 'event', field: 'event.kind', values: ['event'], - originalValue: 'event', + originalValue: ['event'], }, { category: 'event', field: 'event.module', values: ['suricata'], - originalValue: 'suricata', + originalValue: ['suricata'], }, { category: 'event', field: 'event.type', values: ['fileinfo'], - originalValue: 'fileinfo', + originalValue: ['fileinfo'], }, { category: 'file', @@ -117,260 +117,261 @@ const EXPECTED_DATA = [ values: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], - originalValue: + originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], }, { category: 'file', field: 'file.size', - values: [48277], - originalValue: 48277, + values: ['48277'], + originalValue: ['48277'], }, { category: 'fileset', field: 'fileset.name', values: ['eve'], - originalValue: 'eve', + originalValue: ['eve'], }, { category: 'flow', field: 'flow.locality', values: ['public'], - originalValue: 'public', + originalValue: ['public'], }, { category: 'host', field: 'host.architecture', values: ['armv7l'], - originalValue: 'armv7l', + originalValue: ['armv7l'], }, { category: 'host', field: 'host.hostname', values: ['raspberrypi'], - originalValue: 'raspberrypi', + originalValue: ['raspberrypi'], }, { category: 'host', field: 'host.id', values: ['b19a781f683541a7a25ee345133aa399'], - originalValue: 'b19a781f683541a7a25ee345133aa399', + originalValue: ['b19a781f683541a7a25ee345133aa399'], }, { category: 'host', field: 'host.name', values: ['raspberrypi'], - originalValue: 'raspberrypi', + originalValue: ['raspberrypi'], }, { category: 'host', field: 'host.os.codename', values: ['stretch'], - originalValue: 'stretch', + originalValue: ['stretch'], }, { category: 'host', field: 'host.os.family', values: [''], - originalValue: '', + originalValue: [''], }, { category: 'host', field: 'host.os.kernel', values: ['4.14.50-v7+'], - originalValue: '4.14.50-v7+', + originalValue: ['4.14.50-v7+'], }, { category: 'host', field: 'host.os.name', values: ['Raspbian GNU/Linux'], - originalValue: 'Raspbian GNU/Linux', + originalValue: ['Raspbian GNU/Linux'], }, { category: 'host', field: 'host.os.platform', values: ['raspbian'], - originalValue: 'raspbian', + originalValue: ['raspbian'], }, { category: 'host', field: 'host.os.version', values: ['9 (stretch)'], - originalValue: '9 (stretch)', + originalValue: ['9 (stretch)'], }, { category: 'http', field: 'http.request.method', values: ['get'], - originalValue: 'get', + originalValue: ['get'], }, { category: 'http', field: 'http.response.body.bytes', - values: [48277], - originalValue: 48277, + values: ['48277'], + originalValue: ['48277'], }, { category: 'http', field: 'http.response.status_code', - values: [206], - originalValue: 206, + values: ['206'], + originalValue: ['206'], }, { category: 'input', field: 'input.type', values: ['log'], - originalValue: 'log', + originalValue: ['log'], }, { category: 'base', field: 'labels.pipeline', values: ['filebeat-7.0.0-suricata-eve-pipeline'], - originalValue: 'filebeat-7.0.0-suricata-eve-pipeline', + originalValue: ['filebeat-7.0.0-suricata-eve-pipeline'], }, { category: 'log', field: 'log.file.path', values: ['/var/log/suricata/eve.json'], - originalValue: '/var/log/suricata/eve.json', + originalValue: ['/var/log/suricata/eve.json'], }, { category: 'log', field: 'log.offset', - values: [1856288115], - originalValue: 1856288115, + values: ['1856288115'], + originalValue: ['1856288115'], }, { category: 'network', field: 'network.name', values: ['iot'], - originalValue: 'iot', + originalValue: ['iot'], }, { category: 'network', field: 'network.protocol', values: ['http'], - originalValue: 'http', + originalValue: ['http'], }, { category: 'network', field: 'network.transport', values: ['tcp'], - originalValue: 'tcp', + originalValue: ['tcp'], }, { category: 'service', field: 'service.type', values: ['suricata'], - originalValue: 'suricata', + originalValue: ['suricata'], }, { category: 'source', field: 'source.as.num', - values: [16509], - originalValue: 16509, + values: ['16509'], + originalValue: ['16509'], }, { category: 'source', field: 'source.as.org', values: ['Amazon.com, Inc.'], - originalValue: 'Amazon.com, Inc.', + originalValue: ['Amazon.com, Inc.'], }, { category: 'source', field: 'source.domain', values: ['server-54-239-219-210.jfk51.r.cloudfront.net'], - originalValue: 'server-54-239-219-210.jfk51.r.cloudfront.net', + originalValue: ['server-54-239-219-210.jfk51.r.cloudfront.net'], }, { category: 'source', field: 'source.geo.city_name', values: ['Seattle'], - originalValue: 'Seattle', + originalValue: ['Seattle'], }, { category: 'source', field: 'source.geo.continent_name', values: ['North America'], - originalValue: 'North America', + originalValue: ['North America'], }, { category: 'source', field: 'source.geo.country_iso_code', values: ['US'], - originalValue: 'US', + originalValue: ['US'], }, { category: 'source', field: 'source.geo.location.lat', - values: [47.6103], - originalValue: 47.6103, + values: ['47.6103'], + originalValue: ['47.6103'], }, { category: 'source', field: 'source.geo.location.lon', - values: [-122.3341], - originalValue: -122.3341, + values: ['-122.3341'], + originalValue: ['-122.3341'], }, { category: 'source', field: 'source.geo.region_iso_code', values: ['US-WA'], - originalValue: 'US-WA', + originalValue: ['US-WA'], }, { category: 'source', field: 'source.geo.region_name', values: ['Washington'], - originalValue: 'Washington', + originalValue: ['Washington'], }, { category: 'source', field: 'source.ip', values: ['54.239.219.210'], - originalValue: '54.239.219.210', + originalValue: ['54.239.219.210'], }, { category: 'source', field: 'source.port', - values: [80], - originalValue: 80, + values: ['80'], + originalValue: ['80'], }, { category: 'suricata', field: 'suricata.eve.fileinfo.state', values: ['CLOSED'], - originalValue: 'CLOSED', + originalValue: ['CLOSED'], }, { category: 'suricata', field: 'suricata.eve.fileinfo.tx_id', - values: [301], - originalValue: 301, + values: ['301'], + originalValue: ['301'], }, { category: 'suricata', field: 'suricata.eve.flow_id', - values: [196625917175466], - originalValue: 196625917175466, + values: ['196625917175466'], + originalValue: ['196625917175466'], }, { category: 'suricata', field: 'suricata.eve.http.http_content_type', values: ['video/mp4'], - originalValue: 'video/mp4', + originalValue: ['video/mp4'], }, { category: 'suricata', field: 'suricata.eve.http.protocol', values: ['HTTP/1.1'], - originalValue: 'HTTP/1.1', + originalValue: ['HTTP/1.1'], }, { category: 'suricata', field: 'suricata.eve.in_iface', values: ['eth0'], - originalValue: 'eth0', + originalValue: ['eth0'], }, { category: 'base', @@ -382,7 +383,7 @@ const EXPECTED_DATA = [ category: 'url', field: 'url.domain', values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], - originalValue: 's3-iad-2.cf.dash.row.aiv-cdn.net', + originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], }, { category: 'url', @@ -390,8 +391,9 @@ const EXPECTED_DATA = [ values: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], - originalValue: + originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], }, { category: 'url', @@ -399,26 +401,27 @@ const EXPECTED_DATA = [ values: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], - originalValue: + originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], }, { category: '_index', field: '_index', values: ['filebeat-7.0.0-iot-2019.06'], - originalValue: 'filebeat-7.0.0-iot-2019.06', + originalValue: ['filebeat-7.0.0-iot-2019.06'], }, { category: '_id', field: '_id', values: ['QRhG1WgBqd-n62SwZYDT'], - originalValue: 'QRhG1WgBqd-n62SwZYDT', + originalValue: ['QRhG1WgBqd-n62SwZYDT'], }, { category: '_score', field: '_score', - values: [1], - originalValue: 1, + values: ['1'], + originalValue: ['1'], }, ]; @@ -452,7 +455,6 @@ export default function ({ getService }: FtrProviderContext) { eventId: ID, }) .expect(200); - expect(sortBy(detailsData, 'name')).to.eql(sortBy(EXPECTED_DATA, 'name')); }); diff --git a/x-pack/test/functional/apps/grok_debugger/grok_debugger.js b/x-pack/test/functional/apps/grok_debugger/grok_debugger.js index 010341cedd3a7..b2a1c5363fcb6 100644 --- a/x-pack/test/functional/apps/grok_debugger/grok_debugger.js +++ b/x-pack/test/functional/apps/grok_debugger/grok_debugger.js @@ -12,8 +12,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['grokDebugger']); - // FLAKY: https://github.com/elastic/kibana/issues/84440 - describe.skip('grok debugger app', function () { + describe('grok debugger app', function () { this.tags('includeFirefox'); before(async () => { await esArchiver.load('empty_kibana'); diff --git a/x-pack/test/functional/apps/lens/dashboard.ts b/x-pack/test/functional/apps/lens/dashboard.ts index 738e45c1cbcf1..5cbd5dff45e1e 100644 --- a/x-pack/test/functional/apps/lens/dashboard.ts +++ b/x-pack/test/functional/apps/lens/dashboard.ts @@ -156,5 +156,43 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await panelActions.clickContextMenuMoreItem(); await testSubjects.existOrFail(ACTION_TEST_SUBJ); }); + + it('unlink lens panel from embeddable library', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('lnsPieVis'); + await find.clickByButtonText('lnsPieVis'); + await dashboardAddPanel.closeAddPanel(); + + const originalPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis'); + await panelActions.unlinkFromLibary(originalPanel); + await testSubjects.existOrFail('unlinkPanelSuccess'); + + const updatedPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(false); + }); + + it('save lens panel to embeddable library', async () => { + const originalPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis'); + await panelActions.saveToLibrary('lnsPieVis - copy', originalPanel); + await testSubjects.click('confirmSaveSavedObjectButton'); + await testSubjects.existOrFail('addPanelToLibrarySuccess'); + + const updatedPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis-copy'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(true); + + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('lnsPieVis'); + await find.existsByLinkText('lnsPieVis'); + }); }); } diff --git a/x-pack/test/functional/apps/maps/embeddable/embeddable_library.js b/x-pack/test/functional/apps/maps/embeddable/embeddable_library.js new file mode 100644 index 0000000000000..40e73f0d8a763 --- /dev/null +++ b/x-pack/test/functional/apps/maps/embeddable/embeddable_library.js @@ -0,0 +1,80 @@ +/* + * 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'; + +export default function ({ getPageObjects, getService }) { + const find = getService('find'); + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'maps', 'visualize']); + const kibanaServer = getService('kibanaServer'); + const security = getService('security'); + const dashboardAddPanel = getService('dashboardAddPanel'); + const dashboardPanelActions = getService('dashboardPanelActions'); + const dashboardVisualizations = getService('dashboardVisualizations'); + + describe('maps in embeddable library', () => { + before(async () => { + await security.testUser.setRoles( + [ + 'test_logstash_reader', + 'global_maps_all', + 'geoshape_data_reader', + 'global_dashboard_all', + 'meta_for_geoshape_data_reader', + ], + false + ); + await kibanaServer.uiSettings.replace({ + defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', + }); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.clickCreateNewLink(); + await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); + await PageObjects.visualize.clickMapsApp(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.maps.waitForLayersToLoad(); + await PageObjects.maps.clickSaveAndReturnButton(); + await PageObjects.dashboard.waitForRenderComplete(); + }); + + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('save map panel to embeddable library', async () => { + await dashboardPanelActions.saveToLibrary('embeddable library map'); + await testSubjects.existOrFail('addPanelToLibrarySuccess'); + + const mapPanel = await testSubjects.find('embeddablePanelHeading-embeddablelibrarymap'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + mapPanel + ); + expect(libraryActionExists).to.be(true); + }); + + it('unlink map panel from embeddable library', async () => { + const originalPanel = await testSubjects.find('embeddablePanelHeading-embeddablelibrarymap'); + await dashboardPanelActions.unlinkFromLibary(originalPanel); + await testSubjects.existOrFail('unlinkPanelSuccess'); + + const updatedPanel = await testSubjects.find('embeddablePanelHeading-embeddablelibrarymap'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(false); + + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('embeddable library map'); + await find.existsByLinkText('embeddable library map'); + await dashboardAddPanel.closeAddPanel(); + }); + }); +} diff --git a/x-pack/test/functional/apps/maps/embeddable/index.js b/x-pack/test/functional/apps/maps/embeddable/index.js index 815de2e081309..9fd4c9db703db 100644 --- a/x-pack/test/functional/apps/maps/embeddable/index.js +++ b/x-pack/test/functional/apps/maps/embeddable/index.js @@ -9,6 +9,7 @@ export default function ({ loadTestFile }) { describe('embeddable', function () { loadTestFile(require.resolve('./save_and_return')); loadTestFile(require.resolve('./dashboard')); + loadTestFile(require.resolve('./embeddable_library')); loadTestFile(require.resolve('./embeddable_state')); loadTestFile(require.resolve('./tooltip_filter_actions')); }); diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index d76afb7ebdc24..dd20ed58afbc6 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -47,6 +47,7 @@ export default function ({ loadTestFile, getService }) { loadTestFile(require.resolve('./es_geo_grid_source')); loadTestFile(require.resolve('./es_pew_pew_source')); loadTestFile(require.resolve('./joins')); + loadTestFile(require.resolve('./mapbox_styles')); loadTestFile(require.resolve('./mvt_scaling')); loadTestFile(require.resolve('./mvt_super_fine')); loadTestFile(require.resolve('./add_layer_panel')); diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js index 094f5335cd05f..49717016f9c60 100644 --- a/x-pack/test/functional/apps/maps/joins.js +++ b/x-pack/test/functional/apps/maps/joins.js @@ -7,8 +7,6 @@ import expect from '@kbn/expect'; -import { MAPBOX_STYLES } from './mapbox_styles'; - const JOIN_PROPERTY_NAME = '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1'; const EXPECTED_JOIN_VALUES = { alpha: 10, @@ -18,10 +16,6 @@ const EXPECTED_JOIN_VALUES = { }; const VECTOR_SOURCE_ID = 'n1t6f'; -const CIRCLE_STYLE_LAYER_INDEX = 0; -const FILL_STYLE_LAYER_INDEX = 2; -const LINE_STYLE_LAYER_INDEX = 3; -const TOO_MANY_FEATURES_LAYER_INDEX = 4; export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); @@ -95,34 +89,6 @@ export default function ({ getPageObjects, getService }) { }); }); - it('should style fills, points, lines, and bounding-boxes independently', async () => { - const mapboxStyle = await PageObjects.maps.getMapboxStyle(); - const layersForVectorSource = mapboxStyle.layers.filter((mbLayer) => { - return mbLayer.id.startsWith(VECTOR_SOURCE_ID); - }); - - //circle layer for points - expect(layersForVectorSource[CIRCLE_STYLE_LAYER_INDEX]).to.eql(MAPBOX_STYLES.POINT_LAYER); - - //fill layer - expect(layersForVectorSource[FILL_STYLE_LAYER_INDEX]).to.eql(MAPBOX_STYLES.FILL_LAYER); - - //line layer for borders - expect(layersForVectorSource[LINE_STYLE_LAYER_INDEX]).to.eql(MAPBOX_STYLES.LINE_LAYER); - - //Too many features layer (this is a static style config) - expect(layersForVectorSource[TOO_MANY_FEATURES_LAYER_INDEX]).to.eql({ - id: 'n1t6f_toomanyfeatures', - type: 'fill', - source: 'n1t6f', - minzoom: 0, - maxzoom: 24, - filter: ['==', ['get', '__kbn_too_many_features__'], true], - layout: { visibility: 'visible' }, - paint: { 'fill-pattern': '__kbn_too_many_features_image_id__', 'fill-opacity': 0.75 }, - }); - }); - it('should flag only the joined features as visible', async () => { const mapboxStyle = await PageObjects.maps.getMapboxStyle(); const vectorSource = mapboxStyle.sources[VECTOR_SOURCE_ID]; diff --git a/x-pack/test/functional/apps/maps/mapbox_styles.js b/x-pack/test/functional/apps/maps/mapbox_styles.js index d4496f13b8bef..b483b95e0ca1f 100644 --- a/x-pack/test/functional/apps/maps/mapbox_styles.js +++ b/x-pack/test/functional/apps/maps/mapbox_styles.js @@ -5,176 +5,242 @@ * 2.0. */ -export const MAPBOX_STYLES = { - POINT_LAYER: { - id: 'n1t6f_circle', - type: 'circle', - source: 'n1t6f', - minzoom: 0, - maxzoom: 24, - filter: [ - 'all', - ['==', ['get', '__kbn_isvisibleduetojoin__'], true], - [ - 'all', - ['!=', ['get', '__kbn_too_many_features__'], true], - ['!=', ['get', '__kbn_is_centroid_feature__'], true], - ['any', ['==', ['geometry-type'], 'Point'], ['==', ['geometry-type'], 'MultiPoint']], - ], - ], - layout: { visibility: 'visible' }, - paint: { - 'circle-color': [ - 'interpolate', - ['linear'], - [ - 'coalesce', +import expect from '@kbn/expect'; + +export default function ({ getPageObjects, getService }) { + const PageObjects = getPageObjects(['maps']); + const inspector = getService('inspector'); + const security = getService('security'); + + describe('mapbox styles', () => { + let mapboxStyle; + before(async () => { + await security.testUser.setRoles( + ['global_maps_all', 'geoshape_data_reader', 'meta_for_geoshape_data_reader'], + false + ); + await PageObjects.maps.loadSavedMap('join example'); + mapboxStyle = await PageObjects.maps.getMapboxStyle(); + }); + + after(async () => { + await inspector.close(); + await security.testUser.restoreDefaults(); + }); + + it('should style circle layer as expected', async () => { + const layer = mapboxStyle.layers.find((mbLayer) => { + return mbLayer.id === 'n1t6f_circle'; + }); + expect(layer).to.eql({ + id: 'n1t6f_circle', + type: 'circle', + source: 'n1t6f', + minzoom: 0, + maxzoom: 24, + filter: [ + 'all', + ['==', ['get', '__kbn_isvisibleduetojoin__'], true], [ - 'case', - [ - '==', - ['feature-state', '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1'], - null, - ], - 2, + 'all', + ['!=', ['get', '__kbn_too_many_features__'], true], + ['!=', ['get', '__kbn_is_centroid_feature__'], true], + ['any', ['==', ['geometry-type'], 'Point'], ['==', ['geometry-type'], 'MultiPoint']], + ], + ], + layout: { visibility: 'visible' }, + paint: { + 'circle-color': [ + 'interpolate', + ['linear'], [ - 'max', + 'coalesce', [ - 'min', + 'case', [ - 'to-number', + '==', [ 'feature-state', '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1', ], + null, + ], + 2, + [ + 'max', + [ + 'min', + [ + 'to-number', + [ + 'feature-state', + '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1', + ], + ], + 12, + ], + 3, ], - 12, ], - 3, + 2, ], + 2, + 'rgba(0,0,0,0)', + 3, + '#ecf1f7', + 4.125, + '#d9e3ef', + 5.25, + '#c5d5e7', + 6.375, + '#b2c7df', + 7.5, + '#9eb9d8', + 8.625, + '#8bacd0', + 9.75, + '#769fc8', + 10.875, + '#6092c0', ], - 2, - ], - 2, - 'rgba(0,0,0,0)', - 3, - '#ecf1f7', - 4.125, - '#d9e3ef', - 5.25, - '#c5d5e7', - 6.375, - '#b2c7df', - 7.5, - '#9eb9d8', - 8.625, - '#8bacd0', - 9.75, - '#769fc8', - 10.875, - '#6092c0', - ], - 'circle-opacity': 0.75, - 'circle-stroke-color': '#41937c', - 'circle-stroke-opacity': 0.75, - 'circle-stroke-width': 1, - 'circle-radius': 10, - }, - }, - FILL_LAYER: { - id: 'n1t6f_fill', - type: 'fill', - source: 'n1t6f', - minzoom: 0, - maxzoom: 24, - filter: [ - 'all', - ['==', ['get', '__kbn_isvisibleduetojoin__'], true], - [ - 'all', - ['!=', ['get', '__kbn_too_many_features__'], true], - ['!=', ['get', '__kbn_is_centroid_feature__'], true], - ['any', ['==', ['geometry-type'], 'Polygon'], ['==', ['geometry-type'], 'MultiPolygon']], - ], - ], - layout: { visibility: 'visible' }, - paint: { - 'fill-color': [ - 'interpolate', - ['linear'], - [ - 'coalesce', + 'circle-opacity': 0.75, + 'circle-stroke-color': '#41937c', + 'circle-stroke-opacity': 0.75, + 'circle-stroke-width': 1, + 'circle-radius': 10, + }, + }); + }); + + it('should style fill layer as expected', async () => { + const layer = mapboxStyle.layers.find((mbLayer) => { + return mbLayer.id === 'n1t6f_fill'; + }); + expect(layer).to.eql({ + id: 'n1t6f_fill', + type: 'fill', + source: 'n1t6f', + minzoom: 0, + maxzoom: 24, + filter: [ + 'all', + ['==', ['get', '__kbn_isvisibleduetojoin__'], true], [ - 'case', + 'all', + ['!=', ['get', '__kbn_too_many_features__'], true], + ['!=', ['get', '__kbn_is_centroid_feature__'], true], [ - '==', - ['feature-state', '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1'], - null, + 'any', + ['==', ['geometry-type'], 'Polygon'], + ['==', ['geometry-type'], 'MultiPolygon'], ], - 2, + ], + ], + layout: { visibility: 'visible' }, + paint: { + 'fill-color': [ + 'interpolate', + ['linear'], [ - 'max', + 'coalesce', [ - 'min', + 'case', [ - 'to-number', + '==', [ 'feature-state', '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1', ], + null, + ], + 2, + [ + 'max', + [ + 'min', + [ + 'to-number', + [ + 'feature-state', + '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1', + ], + ], + 12, + ], + 3, ], - 12, ], - 3, + 2, + ], + 2, + 'rgba(0,0,0,0)', + 3, + '#ecf1f7', + 4.125, + '#d9e3ef', + 5.25, + '#c5d5e7', + 6.375, + '#b2c7df', + 7.5, + '#9eb9d8', + 8.625, + '#8bacd0', + 9.75, + '#769fc8', + 10.875, + '#6092c0', + ], + 'fill-opacity': 0.75, + }, + }); + }); + + it('should style fill layer as expected', async () => { + const layer = mapboxStyle.layers.find((mbLayer) => { + return mbLayer.id === 'n1t6f_line'; + }); + expect(layer).to.eql({ + id: 'n1t6f_line', + type: 'line', + source: 'n1t6f', + minzoom: 0, + maxzoom: 24, + filter: [ + 'all', + ['==', ['get', '__kbn_isvisibleduetojoin__'], true], + [ + 'all', + ['!=', ['get', '__kbn_too_many_features__'], true], + ['!=', ['get', '__kbn_is_centroid_feature__'], true], + [ + 'any', + ['==', ['geometry-type'], 'Polygon'], + ['==', ['geometry-type'], 'MultiPolygon'], + ['==', ['geometry-type'], 'LineString'], + ['==', ['geometry-type'], 'MultiLineString'], ], ], - 2, - ], - 2, - 'rgba(0,0,0,0)', - 3, - '#ecf1f7', - 4.125, - '#d9e3ef', - 5.25, - '#c5d5e7', - 6.375, - '#b2c7df', - 7.5, - '#9eb9d8', - 8.625, - '#8bacd0', - 9.75, - '#769fc8', - 10.875, - '#6092c0', - ], - 'fill-opacity': 0.75, - }, - }, - LINE_LAYER: { - id: 'n1t6f_line', - type: 'line', - source: 'n1t6f', - minzoom: 0, - maxzoom: 24, - filter: [ - 'all', - ['==', ['get', '__kbn_isvisibleduetojoin__'], true], - [ - 'all', - ['!=', ['get', '__kbn_too_many_features__'], true], - ['!=', ['get', '__kbn_is_centroid_feature__'], true], - [ - 'any', - ['==', ['geometry-type'], 'Polygon'], - ['==', ['geometry-type'], 'MultiPolygon'], - ['==', ['geometry-type'], 'LineString'], - ['==', ['geometry-type'], 'MultiLineString'], ], - ], - ], - layout: { visibility: 'visible' }, - paint: { 'line-color': '#41937c', 'line-opacity': 0.75, 'line-width': 1 }, - }, -}; + layout: { visibility: 'visible' }, + paint: { 'line-color': '#41937c', 'line-opacity': 0.75, 'line-width': 1 }, + }); + }); + + it('should style incomplete data layer as expected', async () => { + const layer = mapboxStyle.layers.find((mbLayer) => { + return mbLayer.id === 'n1t6f_toomanyfeatures'; + }); + expect(layer).to.eql({ + id: 'n1t6f_toomanyfeatures', + type: 'fill', + source: 'n1t6f', + minzoom: 0, + maxzoom: 24, + filter: ['==', ['get', '__kbn_too_many_features__'], true], + layout: { visibility: 'visible' }, + paint: { 'fill-pattern': '__kbn_too_many_features_image_id__', 'fill-opacity': 0.75 }, + }); + }); + }); +} diff --git a/x-pack/test/functional/services/grok_debugger.js b/x-pack/test/functional/services/grok_debugger.js index 730b4ca60c05a..42a80edd70c85 100644 --- a/x-pack/test/functional/services/grok_debugger.js +++ b/x-pack/test/functional/services/grok_debugger.js @@ -13,7 +13,7 @@ export function GrokDebuggerProvider({ getService }) { const retry = getService('retry'); // test subject selectors - const SUBJ_CONTAINER = 'grokDebugger'; + const SUBJ_CONTAINER = 'grokDebuggerContainer'; const SUBJ_UI_ACE_EVENT_INPUT = `${SUBJ_CONTAINER} > aceEventInput > codeEditorContainer`; const SUBJ_UI_ACE_PATTERN_INPUT = `${SUBJ_CONTAINER} > acePatternInput > codeEditorContainer`; @@ -49,10 +49,8 @@ export function GrokDebuggerProvider({ getService }) { } async assertExists() { - await retry.try(async () => { - if (!(await testSubjects.exists(SUBJ_CONTAINER))) { - throw new Error('Expected to find the grok debugger'); - } + await retry.waitFor('Grok Debugger to exist', async () => { + return await testSubjects.exists(SUBJ_CONTAINER); }); } diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 0a7a30f373e07..2981346e80e1d 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -76,6 +76,10 @@ { "path": "../plugins/triggers_actions_ui/tsconfig.json" }, { "path": "../plugins/ui_actions_enhanced/tsconfig.json" }, { "path": "../plugins/upgrade_assistant/tsconfig.json" }, - { "path": "../plugins/watcher/tsconfig.json" } + { "path": "../plugins/watcher/tsconfig.json" }, + { "path": "../plugins/runtime_fields/tsconfig.json" }, + { "path": "../plugins/index_management/tsconfig.json" }, + { "path": "../plugins/watcher/tsconfig.json" }, + { "path": "../plugins/uptime/tsconfig.json" } ] } diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 5d51c2923abd0..740bac3f1b0de 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -56,6 +56,7 @@ "plugins/index_management/**/*", "plugins/grokdebugger/**/*", "plugins/upgrade_assistant/**/*", + "plugins/uptime/**/*", "test/**/*" ], "compilerOptions": { @@ -145,6 +146,7 @@ { "path": "./plugins/upgrade_assistant/tsconfig.json" }, { "path": "./plugins/runtime_fields/tsconfig.json" }, { "path": "./plugins/index_management/tsconfig.json" }, - { "path": "./plugins/watcher/tsconfig.json" } + { "path": "./plugins/watcher/tsconfig.json" }, + { "path": "./plugins/uptime/tsconfig.json" } ] } diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json index ae88ab6486e64..7a2eebc78b69b 100644 --- a/x-pack/tsconfig.refs.json +++ b/x-pack/tsconfig.refs.json @@ -50,6 +50,7 @@ { "path": "./plugins/upgrade_assistant/tsconfig.json" }, { "path": "./plugins/runtime_fields/tsconfig.json" }, { "path": "./plugins/index_management/tsconfig.json" }, - { "path": "./plugins/watcher/tsconfig.json" } + { "path": "./plugins/watcher/tsconfig.json" }, + { "path": "./plugins/uptime/tsconfig.json" } ] }