diff --git a/.buildkite/pipelines/artifacts_container_image.yml b/.buildkite/pipelines/artifacts_container_image.yml index a1d44ca3ce612..972fada9ddde2 100644 --- a/.buildkite/pipelines/artifacts_container_image.yml +++ b/.buildkite/pipelines/artifacts_container_image.yml @@ -1,6 +1,6 @@ steps: - command: .buildkite/scripts/steps/artifacts/docker_image.sh - label: Build default container images + label: Build serverless container images agents: queue: n2-16-spot timeout_in_minutes: 60 diff --git a/.buildkite/scripts/build_kibana.sh b/.buildkite/scripts/build_kibana.sh index 252daa79e0ea8..01810d26758c2 100755 --- a/.buildkite/scripts/build_kibana.sh +++ b/.buildkite/scripts/build_kibana.sh @@ -41,32 +41,6 @@ if is_pr_with_label "ci:build-cloud-image"; then EOF fi -if is_pr_with_label "ci:build-serverless-image"; then - echo "$KIBANA_DOCKER_PASSWORD" | docker login -u "$KIBANA_DOCKER_USERNAME" --password-stdin docker.elastic.co - GIT_ABBREV_COMMIT=${BUILDKITE_COMMIT:0:12} - node scripts/build \ - --skip-initialize \ - --skip-generic-folders \ - --skip-platform-folders \ - --skip-archives \ - --docker-images \ - --docker-namespace="kibana-ci" \ - --docker-tag="pr-$BUILDKITE_PULL_REQUEST-$GIT_ABBREV_COMMIT" \ - --docker-push \ - --skip-docker-ubi \ - --skip-docker-ubuntu \ - --skip-docker-cloud \ - --skip-docker-contexts - docker logout docker.elastic.co - - SERVERLESS_IMAGE=$(docker images --format "{{.Repository}}:{{.Tag}}" docker.elastic.co/kibana-ci/kibana-serverless) - buildkite-agent meta-data set pr_comment:deploy_cloud:head "* Kibana Serverless Image: \`$SERVERLESS_IMAGE\`" - cat << EOF | buildkite-agent annotate --style "info" --context kibana-serverless-image - - Kibana Serverless Image: \`$SERVERLESS_IMAGE\` -EOF -fi - echo "--- Archive Kibana Distribution" linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" installDir="$KIBANA_DIR/install/kibana" diff --git a/.buildkite/scripts/pipelines/pull_request/pipeline.ts b/.buildkite/scripts/pipelines/pull_request/pipeline.ts index 8ee5588b64330..3190f5650b2e0 100644 --- a/.buildkite/scripts/pipelines/pull_request/pipeline.ts +++ b/.buildkite/scripts/pipelines/pull_request/pipeline.ts @@ -170,6 +170,10 @@ const uploadPipeline = (pipelineContent: string | object) => { pipeline.push(getPipeline('.buildkite/pipelines/pull_request/deploy_cloud.yml')); } + if (GITHUB_PR_LABELS.includes('ci:build-serverless-image')) { + pipeline.push(getPipeline('.buildkite/pipelines/artifacts_container_image.yml')); + } + if ( (await doAnyChangesMatch([/.*stor(ies|y).*/])) || GITHUB_PR_LABELS.includes('ci:build-storybooks') diff --git a/.buildkite/scripts/steps/artifacts/docker_image.sh b/.buildkite/scripts/steps/artifacts/docker_image.sh index 6c68f616c23c6..3743aedfdc655 100755 --- a/.buildkite/scripts/steps/artifacts/docker_image.sh +++ b/.buildkite/scripts/steps/artifacts/docker_image.sh @@ -7,12 +7,19 @@ set -euo pipefail source .buildkite/scripts/steps/artifacts/env.sh GIT_ABBREV_COMMIT=${BUILDKITE_COMMIT:0:12} -KIBANA_IMAGE="docker.elastic.co/kibana-ci/kibana-serverless:git-$GIT_ABBREV_COMMIT" +if [[ "${BUILDKITE_PULL_REQUEST:-false}" == "false" ]]; then + KIBANA_IMAGE_TAG="git-$GIT_ABBREV_COMMIT" +else + KIBANA_IMAGE_TAG="pr-$BUILDKITE_PULL_REQUEST-$GIT_ABBREV_COMMIT" +fi + +KIBANA_IMAGE="docker.elastic.co/kibana-ci/kibana-serverless:$KIBANA_IMAGE_TAG" echo "--- Verify manifest does not already exist" echo "$KIBANA_DOCKER_PASSWORD" | docker login -u "$KIBANA_DOCKER_USERNAME" --password-stdin docker.elastic.co trap 'docker logout docker.elastic.co' EXIT +echo "Checking manifest for $KIBANA_IMAGE" if docker manifest inspect $KIBANA_IMAGE &> /dev/null; then echo "Manifest already exists, exiting" exit 1 @@ -25,7 +32,7 @@ node scripts/build \ --docker-cross-compile \ --docker-images \ --docker-namespace="kibana-ci" \ - --docker-tag="git-$GIT_ABBREV_COMMIT" \ + --docker-tag="$KIBANA_IMAGE_TAG" \ --skip-docker-ubuntu \ --skip-docker-ubi \ --skip-docker-cloud \ @@ -55,7 +62,7 @@ docker manifest push "$KIBANA_IMAGE" docker logout docker.elastic.co cat << EOF | buildkite-agent annotate --style "info" --context image - ### Container Images + ### Serverless Images Manifest: \`$KIBANA_IMAGE\` @@ -64,6 +71,11 @@ cat << EOF | buildkite-agent annotate --style "info" --context image ARM64: \`$KIBANA_IMAGE-arm64\` EOF +if [[ "${BUILDKITE_PULL_REQUEST:-false}" != "false" ]]; then + buildkite-agent meta-data set pr_comment:build_serverless:head "* Kibana Serverless Image: \`$KIBANA_IMAGE\`" + buildkite-agent meta-data set pr_comment:early_comment_job_id "$BUILDKITE_JOB_ID" +fi + echo "--- Build dependencies report" node scripts/licenses_csv_report "--csv=target/dependencies-$GIT_ABBREV_COMMIT.csv" @@ -80,7 +92,7 @@ cd - # so that new stack instances contain the latest and greatest image of kibana, # and the respective stack components of course. echo "--- Trigger image tag update" -if [[ "$BUILDKITE_BRANCH" == "$KIBANA_BASE_BRANCH" ]]; then +if [[ "$BUILDKITE_BRANCH" == "$KIBANA_BASE_BRANCH" ]] && [[ "${BUILDKITE_PULL_REQUEST:-false}" == "false" ]]; then cat << EOF | buildkite-agent pipeline upload steps: - label: ":argo: Update kibana image tag for kibana-controller using gpctl" diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0ba3f7fdcc32a..15a828ac76a94 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -589,6 +589,7 @@ src/plugins/screenshot_mode @elastic/appex-sharedux x-pack/examples/screenshotting_example @elastic/appex-sharedux x-pack/plugins/screenshotting @elastic/kibana-reporting-services examples/search_examples @elastic/kibana-data-discovery +packages/kbn-search-response-warnings @elastic/kibana-data-discovery x-pack/plugins/searchprofiler @elastic/platform-deployment-management x-pack/test/security_api_integration/packages/helpers @elastic/kibana-core x-pack/plugins/security @elastic/kibana-security diff --git a/.i18nrc.json b/.i18nrc.json index 8ae8df0439409..02337d5b8f0d4 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -92,6 +92,7 @@ "server": "src/legacy/server", "share": "src/plugins/share", "sharedUXPackages": "packages/shared-ux", + "searchResponseWarnings": "packages/kbn-search-response-warnings", "securitySolutionPackages": "x-pack/packages/security-solution", "serverlessPackages": "packages/serverless", "coloring": "packages/kbn-coloring/src", diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index 0b5c0d0bc3634..baec139453143 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -57,7 +57,11 @@ yarn_install( quiet = False, frozen_lockfile = False, environment = { + "GECKODRIVER_CDNURL": "https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache", + "CHROMEDRIVER_CDNURL": "https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache", + "CHROMEDRIVER_CDNBINARIESURL": "https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache", "SASS_BINARY_SITE": "https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-sass", "RE2_DOWNLOAD_MIRROR": "https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-re2", + "CYPRESS_DOWNLOAD_MIRROR": "https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/cypress", } ) diff --git a/docs/api/data-views/create.asciidoc b/docs/api/data-views/create.asciidoc index 6a358c09bd162..57e526a3c97bd 100644 --- a/docs/api/data-views/create.asciidoc +++ b/docs/api/data-views/create.asciidoc @@ -6,6 +6,11 @@ experimental[] Create data views. +[NOTE] +==== +For the most up-to-date API details, refer to the +{kib-repo}/tree/{branch}/src/plugins/data_views/docs/openapi[open API specification]. +==== [[data-views-api-create-request]] ==== Request diff --git a/docs/api/data-views/default-get.asciidoc b/docs/api/data-views/default-get.asciidoc index 51e5bf60d7097..4a80350d5ea63 100644 --- a/docs/api/data-views/default-get.asciidoc +++ b/docs/api/data-views/default-get.asciidoc @@ -6,6 +6,11 @@ experimental[] Retrieve a default data view ID. Kibana UI uses the default data view unless user picks a different one. +[NOTE] +==== +For the most up-to-date API details, refer to the +{kib-repo}/tree/{branch}/src/plugins/data_views/docs/openapi[open API specification]. +==== [[data-views-api-default-get-request]] ==== Request diff --git a/docs/api/data-views/default-set.asciidoc b/docs/api/data-views/default-set.asciidoc index dd62f859f7220..e03d7f38d5199 100644 --- a/docs/api/data-views/default-set.asciidoc +++ b/docs/api/data-views/default-set.asciidoc @@ -7,6 +7,11 @@ experimental[] Set a default data view ID. Kibana UI will use the default data view unless user picks a different one. The API doesn't validate if given `data_view_id` is a valid id. +[NOTE] +==== +For the most up-to-date API details, refer to the +{kib-repo}/tree/{branch}/src/plugins/data_views/docs/openapi[open API specification]. +==== [[data-views-api-default-set-request]] ==== Request diff --git a/docs/api/data-views/delete.asciidoc b/docs/api/data-views/delete.asciidoc index a3165c799243d..cdd1b50642193 100644 --- a/docs/api/data-views/delete.asciidoc +++ b/docs/api/data-views/delete.asciidoc @@ -8,6 +8,11 @@ experimental[] Delete data views. WARNING: Once you delete a data view, _it cannot be recovered_. +[NOTE] +==== +For the most up-to-date API details, refer to the +{kib-repo}/tree/{branch}/src/plugins/data_views/docs/openapi[open API specification]. +==== [[data-views-api-delete-request]] ==== Request diff --git a/docs/api/data-views/get-all.asciidoc b/docs/api/data-views/get-all.asciidoc index 42727c38f6d98..2511b084953cb 100644 --- a/docs/api/data-views/get-all.asciidoc +++ b/docs/api/data-views/get-all.asciidoc @@ -6,6 +6,12 @@ experimental[] Retrieve a list of all data views. +[NOTE] +==== +For the most up-to-date API details, refer to the +{kib-repo}/tree/{branch}/src/plugins/data_views/docs/openapi[open API specification]. +==== + [[data-views-api-get-all-request]] ==== Request diff --git a/docs/api/data-views/get.asciidoc b/docs/api/data-views/get.asciidoc index 9d6a4160aeacf..81f0ef536e614 100644 --- a/docs/api/data-views/get.asciidoc +++ b/docs/api/data-views/get.asciidoc @@ -6,6 +6,11 @@ experimental[] Retrieve a single data view by ID. +[NOTE] +==== +For the most up-to-date API details, refer to the +{kib-repo}/tree/{branch}/src/plugins/data_views/docs/openapi[open API specification]. +==== [[data-views-api-get-request]] ==== Request diff --git a/docs/api/data-views/runtime-fields/get.asciidoc b/docs/api/data-views/runtime-fields/get.asciidoc index 831744fdc06b5..95e405c2d7742 100644 --- a/docs/api/data-views/runtime-fields/get.asciidoc +++ b/docs/api/data-views/runtime-fields/get.asciidoc @@ -6,6 +6,11 @@ experimental[] Get a runtime field +[NOTE] +==== +For the most up-to-date API details, refer to the +{kib-repo}/tree/{branch}/src/plugins/data_views/docs/openapi[open API specification]. +==== [[data-views-runtime-field-get-request]] ==== Request diff --git a/docs/api/data-views/update.asciidoc b/docs/api/data-views/update.asciidoc index 4c8cd9a6b9db0..8d1e6013b3c0e 100644 --- a/docs/api/data-views/update.asciidoc +++ b/docs/api/data-views/update.asciidoc @@ -7,6 +7,11 @@ experimental[] Update part of an data view. Only the specified fields are updated in the data view. Unspecified fields stay as they are persisted. +[NOTE] +==== +For the most up-to-date API details, refer to the +{kib-repo}/tree/{branch}/src/plugins/data_views/docs/openapi[open API specification]. +==== [[data-views-api-update-request]] ==== Request diff --git a/examples/bfetch_explorer/public/components/page/index.tsx b/examples/bfetch_explorer/public/components/page/index.tsx index b4ce0806b1356..921f4b5fa1635 100644 --- a/examples/bfetch_explorer/public/components/page/index.tsx +++ b/examples/bfetch_explorer/public/components/page/index.tsx @@ -7,34 +7,23 @@ */ import * as React from 'react'; -import { - EuiPageBody, - EuiPageContent_Deprecated as EuiPageContent, - EuiPageContentBody_Deprecated as EuiPageContentBody, - EuiPageHeader, - EuiPageHeaderSection, - EuiTitle, -} from '@elastic/eui'; +import { EuiPageTemplate, EuiPageSection, EuiPageHeader } from '@elastic/eui'; export interface PageProps { title?: React.ReactNode; + sidebar?: React.ReactNode; } -export const Page: React.FC = ({ title = 'Untitled', children }) => { +export const Page: React.FC = ({ title = 'Untitled', sidebar, children }) => { return ( - - - - -

{title}

-
-
-
- - - {children} - - -
+ + {sidebar} + + + + + {children} + + ); }; diff --git a/examples/bfetch_explorer/public/containers/app/index.tsx b/examples/bfetch_explorer/public/containers/app/index.tsx index 54395ff4eb46d..1c6c6c208f7b1 100644 --- a/examples/bfetch_explorer/public/containers/app/index.tsx +++ b/examples/bfetch_explorer/public/containers/app/index.tsx @@ -11,7 +11,6 @@ import { Redirect } from 'react-router-dom'; import { BrowserRouter as Router, Route, Routes } from '@kbn/shared-ux-router'; import { EuiPage } from '@elastic/eui'; import { useDeps } from '../../hooks/use_deps'; -import { Sidebar } from './sidebar'; import { routes } from '../../routes'; export const App: React.FC = () => { @@ -27,7 +26,6 @@ export const App: React.FC = () => { return ( - {routeElements} diff --git a/examples/bfetch_explorer/public/containers/app/pages/page_count_until/index.tsx b/examples/bfetch_explorer/public/containers/app/pages/page_count_until/index.tsx index 75a20904256b5..b0ed74a570bb4 100644 --- a/examples/bfetch_explorer/public/containers/app/pages/page_count_until/index.tsx +++ b/examples/bfetch_explorer/public/containers/app/pages/page_count_until/index.tsx @@ -11,6 +11,7 @@ import { EuiPanel, EuiText } from '@elastic/eui'; import { CountUntil } from '../../../../components/count_until'; import { Page } from '../../../../components/page'; import { useDeps } from '../../../../hooks/use_deps'; +import { Sidebar } from '../../sidebar'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface Props {} @@ -19,7 +20,7 @@ export const PageCountUntil: React.FC = () => { const { plugins } = useDeps(); return ( - + }> This demo sends a single number N using fetchStreaming to the server. The server will stream back N number of messages with 1 second delay each containing a number diff --git a/examples/bfetch_explorer/public/containers/app/pages/page_double_integers/index.tsx b/examples/bfetch_explorer/public/containers/app/pages/page_double_integers/index.tsx index 126e099098cee..0af8218708cbb 100644 --- a/examples/bfetch_explorer/public/containers/app/pages/page_double_integers/index.tsx +++ b/examples/bfetch_explorer/public/containers/app/pages/page_double_integers/index.tsx @@ -11,6 +11,7 @@ import { EuiPanel, EuiText } from '@elastic/eui'; import { DoubleIntegers } from '../../../../components/double_integers'; import { Page } from '../../../../components/page'; import { useDeps } from '../../../../hooks/use_deps'; +import { Sidebar } from '../../sidebar'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface Props {} @@ -19,7 +20,7 @@ export const PageDoubleIntegers: React.FC = () => { const { explorer } = useDeps(); return ( - + }> Below is a list of numbers in milliseconds. They are sent as a batch to the server. For each number server waits given number of milliseconds then doubles the number and streams it diff --git a/examples/bfetch_explorer/public/containers/app/sidebar/index.tsx b/examples/bfetch_explorer/public/containers/app/sidebar/index.tsx index fe0902f88f321..dfd17d18388ae 100644 --- a/examples/bfetch_explorer/public/containers/app/sidebar/index.tsx +++ b/examples/bfetch_explorer/public/containers/app/sidebar/index.tsx @@ -7,7 +7,7 @@ */ import React from 'react'; -import { EuiPageSideBar_Deprecated as EuiPageSideBar, EuiSideNav } from '@elastic/eui'; +import { EuiSideNav } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { routes } from '../../../routes'; @@ -18,26 +18,24 @@ export const Sidebar: React.FC = () => { const history = useHistory(); return ( - - ({ - id, - name: title, - isSelected: true, - items: items.map((route) => ({ - id: route.id, - name: route.title, - onClick: () => history.push(`/${route.id}`), - 'data-test-subj': route.id, - })), + ({ + id, + name: title, + isSelected: true, + items: items.map((route) => ({ + id: route.id, + name: route.title, + onClick: () => history.push(`/${route.id}`), + 'data-test-subj': route.id, })), - }, - ]} - /> - + })), + }, + ]} + /> ); }; diff --git a/examples/developer_examples/public/app.tsx b/examples/developer_examples/public/app.tsx index 15fa925a0f56b..b95806edb28eb 100644 --- a/examples/developer_examples/public/app.tsx +++ b/examples/developer_examples/public/app.tsx @@ -11,9 +11,9 @@ import ReactDOM from 'react-dom'; import { EuiText, - EuiPageContent_Deprecated as EuiPageContent, + EuiPageTemplate, EuiCard, - EuiPageContentHeader_Deprecated as EuiPageContentHeader, + EuiPageHeader, EuiFlexGroup, EuiFlexItem, EuiFieldSearch, @@ -44,59 +44,66 @@ function DeveloperExamples({ examples, navigateToApp, getUrlForApp }: Props) { }); return ( - - - -

Developer examples

-

- The following examples showcase services and APIs that are available to developers. + <> + + + + + + The following examples showcase services and APIs that are available to developers. + + + setSearch(e.target.value)} isClearable={true} aria-label="Search developer examples" /> -

-
-
- - {filteredExamples.map((def) => ( - - - {def.description} - - } - title={ - - { - navigateToApp(def.appId); - }} - > - - {def.title} - - - - window.open(getUrlForApp(def.appId), '_blank', 'noopener, noreferrer') - } - > - Open in new tab - - - } - image={def.image} - footer={def.links ? : undefined} - /> - ))} - -
+ + + + + {filteredExamples.map((def) => ( + + + {def.description} + + } + title={ + + { + navigateToApp(def.appId); + }} + > + + {def.title} + + + + window.open(getUrlForApp(def.appId), '_blank', 'noopener, noreferrer') + } + > + Open in new tab + + + } + image={def.image} + footer={def.links ? : undefined} + /> + + ))} + + + ); } diff --git a/package.json b/package.json index 8962b9736814b..9c9b768f0dc55 100644 --- a/package.json +++ b/package.json @@ -592,6 +592,7 @@ "@kbn/screenshotting-example-plugin": "link:x-pack/examples/screenshotting_example", "@kbn/screenshotting-plugin": "link:x-pack/plugins/screenshotting", "@kbn/search-examples-plugin": "link:examples/search_examples", + "@kbn/search-response-warnings": "link:packages/kbn-search-response-warnings", "@kbn/searchprofiler-plugin": "link:x-pack/plugins/searchprofiler", "@kbn/security-plugin": "link:x-pack/plugins/security", "@kbn/security-solution-ess": "link:x-pack/plugins/security_solution_ess", diff --git a/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_slo_schema.ts b/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_slo_schema.ts index 0670f39058724..82ce866631583 100644 --- a/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_slo_schema.ts +++ b/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_slo_schema.ts @@ -81,6 +81,7 @@ const ObservabilitySloAlertOptional = rt.partial({ }), slo: rt.partial({ id: schemaString, + instanceId: schemaString, revision: schemaStringOrNumber, }), }); diff --git a/packages/kbn-search-response-warnings/README.md b/packages/kbn-search-response-warnings/README.md new file mode 100644 index 0000000000000..527df7dab0f3a --- /dev/null +++ b/packages/kbn-search-response-warnings/README.md @@ -0,0 +1,3 @@ +# @kbn/search-response-warnings + +Components and utils to render warnings which happen when executing search request. For example, shard failures and time outs. diff --git a/packages/kbn-search-response-warnings/index.ts b/packages/kbn-search-response-warnings/index.ts new file mode 100644 index 0000000000000..8ef18d86b9a92 --- /dev/null +++ b/packages/kbn-search-response-warnings/index.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +export type { SearchResponseInterceptedWarning } from './src/types'; + +export { + SearchResponseWarnings, + type SearchResponseWarningsProps, +} from './src/components/search_response_warnings'; + +export { + getSearchResponseInterceptedWarnings, + removeInterceptedWarningDuplicates, +} from './src/utils/get_search_response_intercepted_warnings'; diff --git a/packages/kbn-search-response-warnings/jest.config.js b/packages/kbn-search-response-warnings/jest.config.js new file mode 100644 index 0000000000000..7b3ab9b639841 --- /dev/null +++ b/packages/kbn-search-response-warnings/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-search-response-warnings'], +}; diff --git a/packages/kbn-search-response-warnings/kibana.jsonc b/packages/kbn-search-response-warnings/kibana.jsonc new file mode 100644 index 0000000000000..bf1e5616172f4 --- /dev/null +++ b/packages/kbn-search-response-warnings/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/search-response-warnings", + "owner": "@elastic/kibana-data-discovery" +} diff --git a/packages/kbn-search-response-warnings/package.json b/packages/kbn-search-response-warnings/package.json new file mode 100644 index 0000000000000..69ccd790806aa --- /dev/null +++ b/packages/kbn-search-response-warnings/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/search-response-warnings", + "private": true, + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0" +} \ No newline at end of file diff --git a/packages/kbn-search-response-warnings/src/__mocks__/search_response_warnings.ts b/packages/kbn-search-response-warnings/src/__mocks__/search_response_warnings.ts new file mode 100644 index 0000000000000..9fe2cf02a1671 --- /dev/null +++ b/packages/kbn-search-response-warnings/src/__mocks__/search_response_warnings.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 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 type { SearchResponseWarning } from '@kbn/data-plugin/public'; + +export const searchResponseTimeoutWarningMock: SearchResponseWarning = { + type: 'timed_out', + message: 'Data might be incomplete because your request timed out', + reason: undefined, +}; + +export const searchResponseShardFailureWarningMock: SearchResponseWarning = { + type: 'shard_failure', + message: '3 of 4 shards failed', + text: 'The data might be incomplete or wrong.', + reason: { + type: 'illegal_argument_exception', + reason: 'Field [__anonymous_] of type [boolean] does not support custom formats', + }, +}; + +export const searchResponseWarningsMock: SearchResponseWarning[] = [ + searchResponseTimeoutWarningMock, + searchResponseShardFailureWarningMock, + { + type: 'shard_failure', + message: '3 of 4 shards failed', + text: 'The data might be incomplete or wrong.', + reason: { + type: 'query_shard_exception', + reason: + 'failed to create query: [.ds-kibana_sample_data_logs-2023.07.11-000001][0] Testing shard failures!', + }, + }, + { + type: 'shard_failure', + message: '1 of 4 shards failed', + text: 'The data might be incomplete or wrong.', + reason: { + type: 'query_shard_exception', + reason: + 'failed to create query: [.ds-kibana_sample_data_logs-2023.07.11-000001][0] Testing shard failures!', + }, + }, +]; diff --git a/packages/kbn-search-response-warnings/src/components/search_response_warnings/__snapshots__/search_response_warnings.test.tsx.snap b/packages/kbn-search-response-warnings/src/components/search_response_warnings/__snapshots__/search_response_warnings.test.tsx.snap new file mode 100644 index 0000000000000..01f917a0e6dbc --- /dev/null +++ b/packages/kbn-search-response-warnings/src/components/search_response_warnings/__snapshots__/search_response_warnings.test.tsx.snap @@ -0,0 +1,508 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SearchResponseWarnings renders "badge" correctly 1`] = ` +
+
+ + + +
+
+`; + +exports[`SearchResponseWarnings renders "callout" correctly 1`] = ` +
+
    +
  • +
    +

    +

    +
    +
    +
    +
    +
    + Data might be incomplete because your request timed out +
    +
    +
    +
    +
    + +
    +
    +

    +

    +
  • +
  • +
    +

    +

    +
    +
    +
    +
    +
    + + 3 of 4 shards failed + +
    +
    +
    +
    +

    + The data might be incomplete or wrong. +

    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +

    +

    +
  • +
  • +
    +

    +

    +
    +
    +
    +
    +
    + + 3 of 4 shards failed + +
    +
    +
    +
    +

    + The data might be incomplete or wrong. +

    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +

    +

    +
  • +
  • +
    +

    +

    +
    +
    +
    +
    +
    + + 1 of 4 shards failed + +
    +
    +
    +
    +

    + The data might be incomplete or wrong. +

    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +

    +

    +
  • +
+
+`; + +exports[`SearchResponseWarnings renders "empty_prompt" correctly 1`] = ` +
+
+
+ +
+
+
+

+ No results found +

+
+
+
    +
  • +
    +
    +
    + Data might be incomplete because your request timed out +
    +
    +
    +
  • +
  • +
    +
    +
    + + 3 of 4 shards failed + +
    +
    +
    +
    +

    + The data might be incomplete or wrong. +

    +
    +
    +
    + +
    +
    +
  • +
  • +
    +
    +
    + + 3 of 4 shards failed + +
    +
    +
    +
    +

    + The data might be incomplete or wrong. +

    +
    +
    +
    + +
    +
    +
  • +
  • +
    +
    +
    + + 1 of 4 shards failed + +
    +
    +
    +
    +

    + The data might be incomplete or wrong. +

    +
    +
    +
    + +
    +
    +
  • +
+
+
+
+
+
+`; diff --git a/packages/kbn-search-response-warnings/src/components/search_response_warnings/index.ts b/packages/kbn-search-response-warnings/src/components/search_response_warnings/index.ts new file mode 100644 index 0000000000000..8a3ed6d05600e --- /dev/null +++ b/packages/kbn-search-response-warnings/src/components/search_response_warnings/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +export { + SearchResponseWarnings, + type SearchResponseWarningsProps, +} from './search_response_warnings'; diff --git a/packages/kbn-search-response-warnings/src/components/search_response_warnings/search_response_warnings.test.tsx b/packages/kbn-search-response-warnings/src/components/search_response_warnings/search_response_warnings.test.tsx new file mode 100644 index 0000000000000..6e3c1b1a0d08d --- /dev/null +++ b/packages/kbn-search-response-warnings/src/components/search_response_warnings/search_response_warnings.test.tsx @@ -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 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 React from 'react'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { SearchResponseWarnings } from './search_response_warnings'; +import { searchResponseWarningsMock } from '../../__mocks__/search_response_warnings'; + +const interceptedWarnings = searchResponseWarningsMock.map((originalWarning, index) => ({ + originalWarning, + action: originalWarning.type === 'shard_failure' ? : undefined, +})); + +describe('SearchResponseWarnings', () => { + it('renders "callout" correctly', () => { + const component = mountWithIntl( + + ); + expect(component.render()).toMatchSnapshot(); + }); + + it('renders "badge" correctly', () => { + const component = mountWithIntl( + + ); + expect(component.render()).toMatchSnapshot(); + }); + + it('renders "empty_prompt" correctly', () => { + const component = mountWithIntl( + + ); + expect(component.render()).toMatchSnapshot(); + }); +}); diff --git a/packages/kbn-search-response-warnings/src/components/search_response_warnings/search_response_warnings.tsx b/packages/kbn-search-response-warnings/src/components/search_response_warnings/search_response_warnings.tsx new file mode 100644 index 0000000000000..3c92096aa982b --- /dev/null +++ b/packages/kbn-search-response-warnings/src/components/search_response_warnings/search_response_warnings.tsx @@ -0,0 +1,313 @@ +/* + * 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 React, { PropsWithChildren, useEffect, useState } from 'react'; +import { + EuiCallOut, + EuiEmptyPrompt, + EuiText, + EuiTextProps, + EuiFlexGroup, + EuiFlexGroupProps, + EuiFlexItem, + EuiToolTip, + EuiButton, + EuiIcon, + EuiPopover, + useEuiTheme, + useEuiFontSize, + EuiButtonIcon, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import { i18n } from '@kbn/i18n'; +import type { SearchResponseInterceptedWarning } from '../../types'; + +/** + * SearchResponseWarnings component props + */ +export interface SearchResponseWarningsProps { + /** + * An array of warnings which can have actions + */ + interceptedWarnings?: SearchResponseInterceptedWarning[]; + + /** + * View variant + */ + variant: 'callout' | 'badge' | 'empty_prompt'; + + /** + * Custom data-test-subj value + */ + 'data-test-subj': string; +} + +/** + * SearchResponseWarnings component + * @param interceptedWarnings + * @param variant + * @param dataTestSubj + * @constructor + */ +export const SearchResponseWarnings = ({ + interceptedWarnings, + variant, + 'data-test-subj': dataTestSubj, +}: SearchResponseWarningsProps) => { + const { euiTheme } = useEuiTheme(); + const xsFontSize = useEuiFontSize('xs').fontSize; + const [isCalloutVisibleMap, setIsCalloutVisibleMap] = useState>({}); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + useEffect(() => { + setIsCalloutVisibleMap({}); + }, [interceptedWarnings, setIsCalloutVisibleMap]); + + if (!interceptedWarnings?.length) { + return null; + } + + if (variant === 'callout') { + return ( +
+
    + {interceptedWarnings.map((warning, index) => { + if (isCalloutVisibleMap[index] === false) { + return null; + } + return ( +
  • + + setIsCalloutVisibleMap((prev) => ({ ...prev, [index]: false })) + } + > + + + } + color="warning" + iconType="warning" + size="s" + css={css` + .euiTitle { + display: flex; + align-items: center; + } + `} + data-test-subj={dataTestSubj} + /> +
  • + ); + })} +
+
+ ); + } + + if (variant === 'empty_prompt') { + return ( + + {i18n.translate('searchResponseWarnings.noResultsTitle', { + defaultMessage: 'No results found', + })} + + } + body={ +
    + {interceptedWarnings.map((warning, index) => ( +
  • + +
  • + ))} +
+ } + /> + ); + } + + if (variant === 'badge') { + const warningCount = interceptedWarnings.length; + const buttonLabel = i18n.translate('searchResponseWarnings.badgeButtonLabel', { + defaultMessage: '{warningCount} {warningCount, plural, one {warning} other {warnings}}', + values: { + warningCount, + }, + }); + + return ( + + setIsPopoverOpen(true)} + data-test-subj={`${dataTestSubj}_trigger`} + title={buttonLabel} + css={css` + block-size: ${euiTheme.size.l}; + font-size: ${xsFontSize}; + padding: 0 ${euiTheme.size.xs}; + & > * { + gap: ${euiTheme.size.xs}; + } + `} + > + + {warningCount} + + + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + > +
    + {interceptedWarnings.map((warning, index) => ( +
  • + + + + + + + + +
  • + ))} +
+
+ ); + } + + return null; +}; + +function WarningContent({ + warning: { originalWarning, action }, + textSize = 's', + groupStyles, + 'data-test-subj': dataTestSubj, +}: { + warning: SearchResponseInterceptedWarning; + textSize?: EuiTextProps['size']; + groupStyles?: Partial; + 'data-test-subj': string; +}) { + const hasDescription = 'text' in originalWarning; + + return ( + + + + {hasDescription ? {originalWarning.message} : originalWarning.message} + + + {hasDescription ? ( + + +

{originalWarning.text}

+
+
+ ) : null} + {action ? {action} : null} +
+ ); +} + +function CalloutTitleWrapper({ + children, + onCloseCallout, +}: PropsWithChildren<{ onCloseCallout: () => void }>) { + return ( + + {children} + + + + + ); +} diff --git a/packages/kbn-search-response-warnings/src/types.ts b/packages/kbn-search-response-warnings/src/types.ts new file mode 100644 index 0000000000000..a0406a050c3c1 --- /dev/null +++ b/packages/kbn-search-response-warnings/src/types.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 type { ReactNode } from 'react'; +import type { SearchResponseWarning } from '@kbn/data-plugin/public'; + +/** + * Search Response Warning type which also includes an action + */ +export interface SearchResponseInterceptedWarning { + originalWarning: SearchResponseWarning; + action?: ReactNode; +} diff --git a/packages/kbn-search-response-warnings/src/utils/get_search_response_intercepted_warnings.test.tsx b/packages/kbn-search-response-warnings/src/utils/get_search_response_intercepted_warnings.test.tsx new file mode 100644 index 0000000000000..34ae546f42ba6 --- /dev/null +++ b/packages/kbn-search-response-warnings/src/utils/get_search_response_intercepted_warnings.test.tsx @@ -0,0 +1,184 @@ +/* + * 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 { RequestAdapter } from '@kbn/inspector-plugin/common'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { coreMock } from '@kbn/core/public/mocks'; +import { + getSearchResponseInterceptedWarnings, + removeInterceptedWarningDuplicates, +} from './get_search_response_intercepted_warnings'; +import { searchResponseWarningsMock } from '../__mocks__/search_response_warnings'; + +const servicesMock = { + data: dataPluginMock.createStartContract(), + theme: coreMock.createStart().theme, +}; + +describe('getSearchResponseInterceptedWarnings', () => { + const adapter = new RequestAdapter(); + + it('should catch warnings correctly', () => { + const services = { + ...servicesMock, + }; + services.data.search.showWarnings = jest.fn((_, callback) => { + // @ts-expect-error for empty meta + callback?.(searchResponseWarningsMock[0], {}); + // @ts-expect-error for empty meta + callback?.(searchResponseWarningsMock[1], {}); + // @ts-expect-error for empty meta + callback?.(searchResponseWarningsMock[2], {}); + // @ts-expect-error for empty meta + callback?.(searchResponseWarningsMock[3], {}); + + // plus duplicates + // @ts-expect-error for empty meta + callback?.(searchResponseWarningsMock[0], {}); + // @ts-expect-error for empty meta + callback?.(searchResponseWarningsMock[1], {}); + // @ts-expect-error for empty meta + callback?.(searchResponseWarningsMock[2], {}); + }); + expect( + getSearchResponseInterceptedWarnings({ + services, + adapter, + options: { + disableShardFailureWarning: true, + }, + }) + ).toMatchInlineSnapshot(` + Array [ + Object { + "action": undefined, + "originalWarning": Object { + "message": "Data might be incomplete because your request timed out", + "reason": undefined, + "type": "timed_out", + }, + }, + Object { + "action": , + "originalWarning": Object { + "message": "3 of 4 shards failed", + "reason": Object { + "reason": "Field [__anonymous_] of type [boolean] does not support custom formats", + "type": "illegal_argument_exception", + }, + "text": "The data might be incomplete or wrong.", + "type": "shard_failure", + }, + }, + Object { + "action": , + "originalWarning": Object { + "message": "3 of 4 shards failed", + "reason": Object { + "reason": "failed to create query: [.ds-kibana_sample_data_logs-2023.07.11-000001][0] Testing shard failures!", + "type": "query_shard_exception", + }, + "text": "The data might be incomplete or wrong.", + "type": "shard_failure", + }, + }, + Object { + "action": , + "originalWarning": Object { + "message": "1 of 4 shards failed", + "reason": Object { + "reason": "failed to create query: [.ds-kibana_sample_data_logs-2023.07.11-000001][0] Testing shard failures!", + "type": "query_shard_exception", + }, + "text": "The data might be incomplete or wrong.", + "type": "shard_failure", + }, + }, + ] + `); + }); + + it('should not catch any warnings if disableShardFailureWarning is false', () => { + const services = { + ...servicesMock, + }; + services.data.search.showWarnings = jest.fn((_, callback) => { + // @ts-expect-error for empty meta + callback?.(searchResponseWarningsMock[0], {}); + }); + expect( + getSearchResponseInterceptedWarnings({ + services, + adapter, + options: { + disableShardFailureWarning: false, + }, + }) + ).toBeUndefined(); + }); +}); + +describe('removeInterceptedWarningDuplicates', () => { + it('should remove duplicates successfully', () => { + const interceptedWarnings = searchResponseWarningsMock.map((originalWarning) => ({ + originalWarning, + })); + + expect(removeInterceptedWarningDuplicates([interceptedWarnings[0]])).toEqual([ + interceptedWarnings[0], + ]); + expect(removeInterceptedWarningDuplicates(interceptedWarnings)).toEqual(interceptedWarnings); + expect( + removeInterceptedWarningDuplicates([...interceptedWarnings, ...interceptedWarnings]) + ).toEqual(interceptedWarnings); + }); + + it('should return undefined if the list is empty', () => { + expect(removeInterceptedWarningDuplicates([])).toBeUndefined(); + expect(removeInterceptedWarningDuplicates(undefined)).toBeUndefined(); + }); +}); diff --git a/packages/kbn-search-response-warnings/src/utils/get_search_response_intercepted_warnings.tsx b/packages/kbn-search-response-warnings/src/utils/get_search_response_intercepted_warnings.tsx new file mode 100644 index 0000000000000..38ad0da2639f7 --- /dev/null +++ b/packages/kbn-search-response-warnings/src/utils/get_search_response_intercepted_warnings.tsx @@ -0,0 +1,88 @@ +/* + * 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 React from 'react'; +import { uniqBy } from 'lodash'; +import { + type DataPublicPluginStart, + type ShardFailureRequest, + ShardFailureOpenModalButton, +} from '@kbn/data-plugin/public'; +import type { RequestAdapter } from '@kbn/inspector-plugin/common'; +import type { CoreStart } from '@kbn/core-lifecycle-browser'; +import type { SearchResponseInterceptedWarning } from '../types'; + +/** + * Intercepts warnings for a search source request + * @param services + * @param adapter + * @param options + */ +export const getSearchResponseInterceptedWarnings = ({ + services, + adapter, + options, +}: { + services: { + data: DataPublicPluginStart; + theme: CoreStart['theme']; + }; + adapter: RequestAdapter; + options?: { + disableShardFailureWarning?: boolean; + }; +}): SearchResponseInterceptedWarning[] | undefined => { + if (!options?.disableShardFailureWarning) { + return undefined; + } + + const interceptedWarnings: SearchResponseInterceptedWarning[] = []; + + services.data.search.showWarnings(adapter, (warning, meta) => { + const { request, response } = meta; + + interceptedWarnings.push({ + originalWarning: warning, + action: + warning.type === 'shard_failure' && warning.text && warning.message ? ( + ({ + request: request as ShardFailureRequest, + response, + })} + color="primary" + isButtonEmpty={true} + /> + ) : undefined, + }); + return true; // suppress the default behaviour + }); + + return removeInterceptedWarningDuplicates(interceptedWarnings); +}; + +/** + * Removes duplicated warnings + * @param interceptedWarnings + */ +export const removeInterceptedWarningDuplicates = ( + interceptedWarnings: SearchResponseInterceptedWarning[] | undefined +): SearchResponseInterceptedWarning[] | undefined => { + if (!interceptedWarnings?.length) { + return undefined; + } + + const uniqInterceptedWarnings = uniqBy(interceptedWarnings, (interceptedWarning) => + JSON.stringify(interceptedWarning.originalWarning) + ); + + return uniqInterceptedWarnings?.length ? uniqInterceptedWarnings : undefined; +}; diff --git a/packages/kbn-search-response-warnings/tsconfig.json b/packages/kbn-search-response-warnings/tsconfig.json new file mode 100644 index 0000000000000..77bffc521e15f --- /dev/null +++ b/packages/kbn-search-response-warnings/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types" + }, + "include": ["*.ts", "src/**/*", "__mocks__/**/*.ts"], + "kbn_references": [ + "@kbn/data-plugin", + "@kbn/test-jest-helpers", + "@kbn/i18n", + "@kbn/inspector-plugin", + "@kbn/core", + "@kbn/core-lifecycle-browser", + ], + "exclude": ["target/**/*"] +} diff --git a/packages/kbn-telemetry-tools/src/schema_ftr_validations/schema_to_config_schema.ts b/packages/kbn-telemetry-tools/src/schema_ftr_validations/schema_to_config_schema.ts index b80a42e101284..5c12c199bbe6d 100644 --- a/packages/kbn-telemetry-tools/src/schema_ftr_validations/schema_to_config_schema.ts +++ b/packages/kbn-telemetry-tools/src/schema_ftr_validations/schema_to_config_schema.ts @@ -59,7 +59,8 @@ function valueSchemaToConfigSchema(value: TelemetrySchemaValue): Type { case 'keyword': case 'text': case 'date': - return schema.string(); + // Some plugins return `null` when there is no value to report + return schema.oneOf([schema.string(), schema.literal(null)]); case 'byte': case 'double': case 'float': diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 2bcf3775183e5..c1942775c88b5 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -128,10 +128,10 @@ export PATH="$PATH:$yarnGlobalDir" # use a proxy to fetch chromedriver/geckodriver asset export GECKODRIVER_CDNURL="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache" -export CHROMEDRIVER_LEGACY_CDNURL="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache" export CHROMEDRIVER_CDNURL="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache" export CHROMEDRIVER_CDNBINARIESURL="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache" export RE2_DOWNLOAD_MIRROR="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache" +export SASS_BINARY_SITE="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-sass" export CYPRESS_DOWNLOAD_MIRROR="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/cypress" export CHECKS_REPORTER_ACTIVE=false diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index 0ba2c77c8c016..a9d48108d296d 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -543,6 +543,8 @@ export class SearchSource { return search({ params, indexType: searchRequest.indexType }, options).pipe( switchMap((response) => { + // For testing timeout messages in UI, uncomment the next line + // response.rawResponse.timed_out = true; return new Observable>((obs) => { if (isErrorResponse(response)) { obs.error(response); @@ -916,6 +918,20 @@ export class SearchSource { }; body.query = buildEsQuery(index, query, filters, esQueryConfigs); + // For testing shard failure messages in UI, uncomment the next block and switch to `kibana*` data view + // body.query = { + // error_query: { + // indices: [ + // { + // name: 'kibana_sample_data_logs', + // shard_ids: [0, 1], + // error_type: 'exception', + // message: 'Testing shard failures!', + // }, + // ], + // }, + // }; + if (highlightAll && body.query) { body.highlight = getHighlightRequest(getConfig(UI_SETTINGS.DOC_HIGHLIGHT)); delete searchRequest.highlightAll; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 621b4ec7c1372..d8aa9a35a29a7 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -272,6 +272,7 @@ export type { GlobalQueryStateFromUrl, } from './query'; +// TODO: move to @kbn/search-response-warnings export type { ShardFailureRequest } from './shard_failure_modal'; export { ShardFailureOpenModalButton } from './shard_failure_modal'; diff --git a/src/plugins/data_views/docs/openapi/README.md b/src/plugins/data_views/docs/openapi/README.md new file mode 100644 index 0000000000000..1480da8537706 --- /dev/null +++ b/src/plugins/data_views/docs/openapi/README.md @@ -0,0 +1,35 @@ +# OpenAPI (Experimental) + +The current self-contained spec file is available as `bundled.json` or `bundled.yaml` and can be used for online tools like those found at . +This spec is experimental and may be incomplete or change later. + +A guide about the openApi specification can be found at [https://swagger.io/docs/specification/about/](https://swagger.io/docs/specification/about/). + + +## The `openapi` folder + +* `entrypoint.yaml` is the overview file which pulls together all the paths and components. +* [Paths](paths/README.md): Defines each endpoint. A path can have one operation per http method. +* [Components](components/README.md): Defines reusable components. + +## Tools + +It is possible to validate the docs before bundling them with the following +command: + +```bash +npx swagger-cli validate entrypoint.yaml +``` + +Then you can generate the `bundled` files by running the following commands: + +```bash +npx @redocly/cli bundle entrypoint.yaml --output bundled.yaml --ext yaml +npx @redocly/cli bundle entrypoint.yaml --output bundled.json --ext json +``` + +After generating the json bundle ensure that it is also valid by running the following command: + +```bash +npx @redocly/cli lint bundled.json +``` diff --git a/src/plugins/data_views/docs/openapi/bundled.json b/src/plugins/data_views/docs/openapi/bundled.json new file mode 100644 index 0000000000000..a4173278937c1 --- /dev/null +++ b/src/plugins/data_views/docs/openapi/bundled.json @@ -0,0 +1,2617 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Data views", + "description": "OpenAPI schema for data view endpoints", + "version": "0.1", + "contact": { + "name": "Kibana Core Team" + }, + "license": { + "name": "Elastic License 2.0", + "url": "https://www.elastic.co/licensing/elastic-license" + } + }, + "servers": [ + { + "url": "http://localhost:5601", + "description": "local" + } + ], + "security": [ + { + "basicAuth": [] + }, + { + "apiKeyAuth": [] + } + ], + "tags": [ + { + "name": "data views", + "description": "Data view APIs enable you to manage data views, formerly known as Kibana index patterns." + } + ], + "paths": { + "/api/data_views": { + "get": { + "summary": "Retrieves a list of all data views.", + "operationId": "getAllDataViews", + "description": "This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.\n", + "tags": [ + "data views" + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data_view": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespaces": { + "type": "array", + "items": { + "type": "string" + } + }, + "title": { + "type": "string" + }, + "typeMeta": { + "type": "object" + } + } + } + } + } + }, + "examples": { + "getAllDataViewsResponse": { + "$ref": "#/components/examples/get_data_views_response" + } + } + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/api/data_views/data_view": { + "post": { + "summary": "Creates a data view.", + "operationId": "createDataView", + "description": "This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.\n", + "tags": [ + "data views" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/create_data_view_request_object" + }, + "examples": { + "createDataViewRequest": { + "$ref": "#/components/examples/create_data_view_request" + } + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/data_view_response_object" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/400_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/api/data_views/data_view/{viewId}": { + "get": { + "summary": "Retrieves a single data view by identifier.", + "operationId": "getDataView", + "description": "This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.\n", + "tags": [ + "data views" + ], + "parameters": [ + { + "$ref": "#/components/parameters/view_id" + } + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/data_view_response_object" + }, + "examples": { + "getDataViewResponse": { + "$ref": "#/components/examples/get_data_view_response" + } + } + } + } + }, + "404": { + "description": "Object is not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404_response" + } + } + } + } + } + }, + "delete": { + "summary": "Deletes a data view.", + "operationId": "deleteDataView", + "description": "WARNING: When you delete a data view, it cannot be recovered. This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.\n", + "tags": [ + "data views" + ], + "parameters": [ + { + "$ref": "#/components/parameters/view_id" + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + }, + "404": { + "description": "Object is not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "post": { + "summary": "Updates a data view.", + "operationId": "updateDataView", + "description": "This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.\n", + "tags": [ + "data views" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/view_id" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/update_data_view_request_object" + }, + "examples": { + "updateDataViewRequest": { + "$ref": "#/components/examples/update_data_view_request" + } + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/data_view_response_object" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/400_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/api/data_views/data_view/{viewId}/runtime_field/{fieldName}": { + "get": { + "summary": "Retrieves a runtime field.", + "operationId": "getRuntimeField", + "description": "This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.\n", + "tags": [ + "data views" + ], + "parameters": [ + { + "$ref": "#/components/parameters/field_name" + }, + { + "$ref": "#/components/parameters/view_id" + } + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data_view": { + "type": "object" + }, + "fields": { + "type": "array", + "items": { + "type": "object" + } + } + } + }, + "examples": { + "getRuntimeFieldResponse": { + "$ref": "#/components/examples/get_runtime_field_response" + } + } + } + } + }, + "404": { + "description": "Object is not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404_response" + } + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/api/data_views/default": { + "get": { + "summary": "Retrieves the default data view identifier.", + "operationId": "getDefaultDataView", + "description": "This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.\n", + "tags": [ + "data views" + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data_view_id": { + "type": "string" + } + } + }, + "examples": { + "getDefaultDataViewResponse": { + "$ref": "#/components/examples/get_default_data_view_response" + } + } + } + } + } + } + }, + "post": { + "summary": "Sets the default data view identifier.", + "operationId": "setDefaultDatailView", + "description": "This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.\n", + "tags": [ + "data views" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data_view_id" + ], + "properties": { + "data_view_id": { + "type": [ + "string", + "null" + ], + "description": "The data view identifier. NOTE: The API does not validate whether it is a valid identifier. Use `null` to unset the default data view.\n" + }, + "force": { + "type": "boolean", + "description": "Update an existing default data view identifier.", + "default": false + } + } + }, + "examples": { + "setDefaultDataViewRequest": { + "$ref": "#/components/examples/set_default_data_view_request" + } + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "acknowledged": { + "type": "boolean" + } + } + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + } + }, + "components": { + "securitySchemes": { + "basicAuth": { + "type": "http", + "scheme": "basic" + }, + "apiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "ApiKey" + } + }, + "examples": { + "get_data_views_response": { + "summary": "The get all data views API returns a list of data views.", + "value": { + "data_view": [ + { + "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", + "namespaces": [ + "default" + ], + "title": "kibana_sample_data_ecommerce", + "typeMeta": {}, + "name": "Kibana Sample Data eCommerce" + }, + { + "id": "d3d7af60-4c81-11e8-b3d7-01146121b73d", + "namespaces": [ + "default" + ], + "title": "kibana_sample_data_flights", + "name": "Kibana Sample Data Flights" + }, + { + "id": "90943e30-9a47-11e8-b64d-95841ca0b247", + "namespaces": [ + "default" + ], + "title": "kibana_sample_data_logs", + "name": "Kibana Sample Data Logs" + } + ] + } + }, + "create_data_view_request": { + "summary": "Create a data view with runtime fields.", + "value": { + "data_view": { + "title": "logstash-*", + "name": "My Logstash data view", + "runtimeFieldMap": { + "runtime_shape_name": { + "type": "keyword", + "script": { + "source": "emit(doc['shape_name'].value)" + } + } + } + } + } + }, + "get_data_view_response": { + "summary": "The get data view API returns a JSON object that contains information about the data view.", + "value": { + "data_view": { + "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", + "version": "WzUsMV0=", + "title": "kibana_sample_data_ecommerce", + "timeFieldName": "order_date", + "sourceFilters": [], + "fields": { + "_id": { + "count": 0, + "name": "_id", + "type": "string", + "esTypes": [ + "_id" + ], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "_index": { + "count": 0, + "name": "_index", + "type": "string", + "esTypes": [ + "_index" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "_score": { + "count": 0, + "name": "_score", + "type": "number", + "scripted": false, + "searchable": false, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "_source": { + "count": 0, + "name": "_source", + "type": "_source", + "esTypes": [ + "_source" + ], + "scripted": false, + "searchable": false, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "_source" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "category": { + "count": 0, + "name": "category", + "type": "string", + "esTypes": [ + "text" + ], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "category.keyword": { + "count": 0, + "name": "category.keyword", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "subType": { + "multi": { + "parent": "category" + } + }, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "currency": { + "count": 0, + "name": "currency", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "customer_birth_date": { + "count": 0, + "name": "customer_birth_date", + "type": "date", + "esTypes": [ + "date" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "date" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "customer_first_name": { + "count": 0, + "name": "customer_first_name", + "type": "string", + "esTypes": [ + "text" + ], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "customer_first_name.keyword": { + "count": 0, + "name": "customer_first_name.keyword", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "subType": { + "multi": { + "parent": "customer_first_name" + } + }, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "customer_full_name": { + "count": 0, + "name": "customer_full_name", + "type": "string", + "esTypes": [ + "text" + ], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "customer_full_name.keyword": { + "count": 0, + "name": "customer_full_name.keyword", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "subType": { + "multi": { + "parent": "customer_full_name" + } + }, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "customer_gender": { + "count": 0, + "name": "customer_gender", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "customer_id": { + "count": 0, + "name": "customer_id", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "customer_last_name": { + "count": 0, + "name": "customer_last_name", + "type": "string", + "esTypes": [ + "text" + ], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "customer_last_name.keyword": { + "count": 0, + "name": "customer_last_name.keyword", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "subType": { + "multi": { + "parent": "customer_last_name" + } + }, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "customer_phone": { + "count": 0, + "name": "customer_phone", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "day_of_week": { + "count": 0, + "name": "day_of_week", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "day_of_week_i": { + "count": 0, + "name": "day_of_week_i", + "type": "number", + "esTypes": [ + "integer" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "email": { + "count": 0, + "name": "email", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "event.dataset": { + "count": 0, + "name": "event.dataset", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "geoip.city_name": { + "count": 0, + "name": "geoip.city_name", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "geoip.continent_name": { + "count": 0, + "name": "geoip.continent_name", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "geoip.country_iso_code": { + "count": 0, + "name": "geoip.country_iso_code", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "geoip.location": { + "count": 0, + "name": "geoip.location", + "type": "geo_point", + "esTypes": [ + "geo_point" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "geo_point", + "params": { + "transform": "wkt" + } + }, + "shortDotsEnable": false, + "isMapped": true + }, + "geoip.region_name": { + "count": 0, + "name": "geoip.region_name", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "manufacturer": { + "count": 0, + "name": "manufacturer", + "type": "string", + "esTypes": [ + "text" + ], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "manufacturer.keyword": { + "count": 0, + "name": "manufacturer.keyword", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "subType": { + "multi": { + "parent": "manufacturer" + } + }, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "order_date": { + "count": 0, + "name": "order_date", + "type": "date", + "esTypes": [ + "date" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "date" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "order_id": { + "count": 0, + "name": "order_id", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products._id": { + "count": 0, + "name": "products._id", + "type": "string", + "esTypes": [ + "text" + ], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products._id.keyword": { + "count": 0, + "name": "products._id.keyword", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "subType": { + "multi": { + "parent": "products._id" + } + }, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.base_price": { + "count": 0, + "name": "products.base_price", + "type": "number", + "esTypes": [ + "half_float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.base_unit_price": { + "count": 0, + "name": "products.base_unit_price", + "type": "number", + "esTypes": [ + "half_float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.category": { + "count": 0, + "name": "products.category", + "type": "string", + "esTypes": [ + "text" + ], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.category.keyword": { + "count": 0, + "name": "products.category.keyword", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "subType": { + "multi": { + "parent": "products.category" + } + }, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.created_on": { + "count": 0, + "name": "products.created_on", + "type": "date", + "esTypes": [ + "date" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "date" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.discount_amount": { + "count": 0, + "name": "products.discount_amount", + "type": "number", + "esTypes": [ + "half_float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.discount_percentage": { + "count": 0, + "name": "products.discount_percentage", + "type": "number", + "esTypes": [ + "half_float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.manufacturer": { + "count": 1, + "name": "products.manufacturer", + "type": "string", + "esTypes": [ + "text" + ], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.manufacturer.keyword": { + "count": 0, + "name": "products.manufacturer.keyword", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "subType": { + "multi": { + "parent": "products.manufacturer" + } + }, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.min_price": { + "count": 0, + "name": "products.min_price", + "type": "number", + "esTypes": [ + "half_float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.price": { + "count": 1, + "name": "products.price", + "type": "number", + "esTypes": [ + "half_float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.product_id": { + "count": 0, + "name": "products.product_id", + "type": "number", + "esTypes": [ + "long" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.product_name": { + "count": 1, + "name": "products.product_name", + "type": "string", + "esTypes": [ + "text" + ], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.product_name.keyword": { + "count": 0, + "name": "products.product_name.keyword", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "subType": { + "multi": { + "parent": "products.product_name" + } + }, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.quantity": { + "count": 0, + "name": "products.quantity", + "type": "number", + "esTypes": [ + "integer" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.sku": { + "count": 0, + "name": "products.sku", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.tax_amount": { + "count": 0, + "name": "products.tax_amount", + "type": "number", + "esTypes": [ + "half_float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.taxful_price": { + "count": 0, + "name": "products.taxful_price", + "type": "number", + "esTypes": [ + "half_float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.taxless_price": { + "count": 0, + "name": "products.taxless_price", + "type": "number", + "esTypes": [ + "half_float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.unit_discount_amount": { + "count": 0, + "name": "products.unit_discount_amount", + "type": "number", + "esTypes": [ + "half_float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "sku": { + "count": 0, + "name": "sku", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "taxful_total_price": { + "count": 0, + "name": "taxful_total_price", + "type": "number", + "esTypes": [ + "half_float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.[00]" + } + }, + "shortDotsEnable": false, + "isMapped": true + }, + "taxless_total_price": { + "count": 0, + "name": "taxless_total_price", + "type": "number", + "esTypes": [ + "half_float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "shortDotsEnable": false, + "isMapped": true + }, + "total_quantity": { + "count": 1, + "name": "total_quantity", + "type": "number", + "esTypes": [ + "integer" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "total_unique_products": { + "count": 0, + "name": "total_unique_products", + "type": "number", + "esTypes": [ + "integer" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "type": { + "count": 0, + "name": "type", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "user": { + "count": 0, + "name": "user", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + } + }, + "typeMeta": {}, + "fieldFormats": { + "taxful_total_price": { + "id": "number", + "params": { + "pattern": "$0,0.[00]" + } + }, + "products.price": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "taxless_total_price": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "products.taxless_price": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "products.taxful_price": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "products.min_price": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "products.base_unit_price": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "products.base_price": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + } + }, + "runtimeFieldMap": {}, + "fieldAttrs": { + "products.manufacturer": { + "count": 1 + }, + "products.price": { + "count": 1 + }, + "products.product_name": { + "count": 1 + }, + "total_quantity": { + "count": 1 + } + }, + "allowNoIndex": false, + "name": "Kibana Sample Data eCommerce", + "namespaces": [ + "default" + ] + } + } + }, + "update_data_view_request": { + "summary": "Update some properties for a data view.", + "value": { + "data_view": { + "title": "kibana_sample_data_ecommerce", + "timeFieldName": "order_date", + "allowNoIndex": false, + "name": "Kibana Sample Data eCommerce" + }, + "refresh_fields": true + } + }, + "get_runtime_field_response": { + "summary": "The get runtime field API returns a JSON object that contains information about the runtime field (`hour_of_day`) and the data view (`d3d7af60-4c81-11e8-b3d7-01146121b73d`).", + "value": { + "fields": [ + { + "count": 0, + "name": "hour_of_day", + "type": "number", + "esTypes": [ + "long" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": false, + "shortDotsEnable": false, + "runtimeField": { + "type": "long", + "script": { + "source": "emit(doc['timestamp'].value.getHour());" + } + } + } + ], + "data_view": { + "id": "d3d7af60-4c81-11e8-b3d7-01146121b73d", + "version": "WzM2LDJd", + "title": "kibana_sample_data_flights", + "timeFieldName": "timestamp", + "sourceFilters": [], + "fields": { + "hour_of_day": { + "count": 0, + "name": "hour_of_day", + "type": "number", + "esTypes": [ + "long" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": false, + "format": { + "id": "number", + "params": { + "pattern": "00" + } + }, + "shortDotsEnable": false, + "runtimeField": { + "type": "long", + "script": { + "source": "emit(doc['timestamp'].value.getHour());" + } + } + }, + "AvgTicketPrice": { + "count": 0, + "name": "AvgTicketPrice", + "type": "number", + "esTypes": [ + "float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.[00]" + } + }, + "shortDotsEnable": false, + "isMapped": true + }, + "Cancelled": { + "count": 0, + "name": "Cancelled", + "type": "boolean", + "esTypes": [ + "boolean" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "boolean" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "Carrier": { + "count": 0, + "name": "Carrier", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "Dest": { + "count": 0, + "name": "Dest", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "DestAirportID": { + "count": 0, + "name": "DestAirportID", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "DestCityName": { + "count": 0, + "name": "DestCityName", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "DestCountry": { + "count": 0, + "name": "DestCountry", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "DestLocation": { + "count": 0, + "name": "DestLocation", + "type": "geo_point", + "esTypes": [ + "geo_point" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "geo_point", + "params": { + "transform": "wkt" + } + }, + "shortDotsEnable": false, + "isMapped": true + }, + "DestRegion": { + "count": 0, + "name": "DestRegion", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "DestWeather": { + "count": 0, + "name": "DestWeather", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "DistanceKilometers": { + "count": 0, + "name": "DistanceKilometers", + "type": "number", + "esTypes": [ + "float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "DistanceMiles": { + "count": 0, + "name": "DistanceMiles", + "type": "number", + "esTypes": [ + "float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "FlightDelay": { + "count": 0, + "name": "FlightDelay", + "type": "boolean", + "esTypes": [ + "boolean" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "boolean" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "FlightDelayMin": { + "count": 0, + "name": "FlightDelayMin", + "type": "number", + "esTypes": [ + "integer" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "FlightDelayType": { + "count": 0, + "name": "FlightDelayType", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "FlightNum": { + "count": 0, + "name": "FlightNum", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "FlightTimeHour": { + "count": 0, + "name": "FlightTimeHour", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "FlightTimeMin": { + "count": 0, + "name": "FlightTimeMin", + "type": "number", + "esTypes": [ + "float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "Origin": { + "count": 0, + "name": "Origin", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "OriginAirportID": { + "count": 0, + "name": "OriginAirportID", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "OriginCityName": { + "count": 0, + "name": "OriginCityName", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "OriginCountry": { + "count": 0, + "name": "OriginCountry", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "OriginLocation": { + "count": 0, + "name": "OriginLocation", + "type": "geo_point", + "esTypes": [ + "geo_point" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "geo_point", + "params": { + "transform": "wkt" + } + }, + "shortDotsEnable": false, + "isMapped": true + }, + "OriginRegion": { + "count": 0, + "name": "OriginRegion", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "OriginWeather": { + "count": 0, + "name": "OriginWeather", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "_id": { + "count": 0, + "name": "_id", + "type": "string", + "esTypes": [ + "_id" + ], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "_index": { + "count": 0, + "name": "_index", + "type": "string", + "esTypes": [ + "_index" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "_score": { + "count": 0, + "name": "_score", + "type": "number", + "scripted": false, + "searchable": false, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "_source": { + "count": 0, + "name": "_source", + "type": "_source", + "esTypes": [ + "_source" + ], + "scripted": false, + "searchable": false, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "_source" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "dayOfWeek": { + "count": 0, + "name": "dayOfWeek", + "type": "number", + "esTypes": [ + "integer" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "timestamp": { + "count": 0, + "name": "timestamp", + "type": "date", + "esTypes": [ + "date" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "date" + }, + "shortDotsEnable": false, + "isMapped": true + } + }, + "fieldFormats": { + "hour_of_day": { + "id": "number", + "params": { + "pattern": "00" + } + }, + "AvgTicketPrice": { + "id": "number", + "params": { + "pattern": "$0,0.[00]" + } + } + }, + "runtimeFieldMap": { + "hour_of_day": { + "type": "long", + "script": { + "source": "emit(doc['timestamp'].value.getHour());" + } + } + }, + "fieldAttrs": {}, + "allowNoIndex": false, + "name": "Kibana Sample Data Flights" + } + } + }, + "get_default_data_view_response": { + "summary": "The get default data view API returns the default data view identifier.", + "value": { + "data_view_id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f" + } + }, + "set_default_data_view_request": { + "summary": "Set the default data view identifier.", + "value": { + "data_view_id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", + "force": true + } + } + }, + "parameters": { + "kbn_xsrf": { + "schema": { + "type": "string" + }, + "in": "header", + "name": "kbn-xsrf", + "description": "Cross-site request forgery protection", + "required": true + }, + "view_id": { + "in": "path", + "name": "viewId", + "description": "An identifier for the data view.", + "required": true, + "schema": { + "type": "string", + "example": "ff959d40-b880-11e8-a6d9-e546fe2bba5f" + } + }, + "field_name": { + "in": "path", + "name": "fieldName", + "description": "The name of the runtime field.", + "required": true, + "schema": { + "type": "string", + "example": "hour_of_day" + } + } + }, + "schemas": { + "allownoindex": { + "type": "boolean", + "description": "Allows the data view saved object to exist before the data is available." + }, + "fieldattrs": { + "type": "object", + "description": "A map of field attributes by field name." + }, + "fieldformats": { + "type": "object", + "description": "A map of field formats by field name." + }, + "namespaces": { + "type": "array", + "description": "An array of space identifiers for sharing the data view between multiple spaces.", + "items": { + "type": "string", + "default": "default" + } + }, + "runtimefieldmap": { + "type": "object", + "description": "A map of runtime field definitions by field name." + }, + "sourcefilters": { + "type": "array", + "description": "The array of field names you want to filter out in Discover." + }, + "timefieldname": { + "type": "string", + "description": "The timestamp field name, which you use for time-based data views." + }, + "title": { + "type": "string", + "description": "Comma-separated list of data streams, indices, and aliases that you want to search. Supports wildcards (`*`)." + }, + "type": { + "type": "string", + "description": "When set to `rollup`, identifies the rollup data views." + }, + "typemeta": { + "type": "object", + "description": "When you use rollup indices, contains the field list for the rollup data view API endpoints." + }, + "create_data_view_request_object": { + "title": "Create data view request", + "type": "object", + "required": [ + "data_view" + ], + "properties": { + "data_view": { + "type": "object", + "required": [ + "title" + ], + "description": "The data view object.", + "properties": { + "allowNoIndex": { + "$ref": "#/components/schemas/allownoindex" + }, + "fieldAttrs": { + "$ref": "#/components/schemas/fieldattrs" + }, + "fieldFormats": { + "$ref": "#/components/schemas/fieldformats" + }, + "fields": { + "type": "object" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string", + "description": "The data view name." + }, + "namespaces": { + "$ref": "#/components/schemas/namespaces" + }, + "runtimeFieldMap": { + "$ref": "#/components/schemas/runtimefieldmap" + }, + "sourceFilters": { + "$ref": "#/components/schemas/sourcefilters" + }, + "timeFieldName": { + "$ref": "#/components/schemas/timefieldname" + }, + "title": { + "$ref": "#/components/schemas/title" + }, + "type": { + "$ref": "#/components/schemas/type" + }, + "typeMeta": { + "$ref": "#/components/schemas/typemeta" + }, + "version": { + "type": "string" + } + } + }, + "override": { + "type": "boolean", + "description": "Override an existing data view if a data view with the provided title already exists.", + "default": false + } + } + }, + "data_view_response_object": { + "title": "Data view response properties", + "type": "object", + "properties": { + "data_view": { + "type": "object", + "properties": { + "allowNoIndex": { + "$ref": "#/components/schemas/allownoindex" + }, + "fieldAttrs": { + "$ref": "#/components/schemas/fieldattrs" + }, + "fieldFormats": { + "$ref": "#/components/schemas/fieldformats" + }, + "fields": { + "type": "object" + }, + "id": { + "type": "string", + "example": "ff959d40-b880-11e8-a6d9-e546fe2bba5f" + }, + "name": { + "type": "string", + "description": "The data view name." + }, + "namespaces": { + "$ref": "#/components/schemas/namespaces" + }, + "runtimeFieldMap": { + "$ref": "#/components/schemas/runtimefieldmap" + }, + "sourceFilters": { + "$ref": "#/components/schemas/sourcefilters" + }, + "timeFieldName": { + "$ref": "#/components/schemas/timefieldname" + }, + "title": { + "$ref": "#/components/schemas/title" + }, + "typeMeta": { + "$ref": "#/components/schemas/typemeta" + }, + "version": { + "type": "string", + "example": "WzQ2LDJd" + } + } + } + } + }, + "400_response": { + "title": "Bad request", + "type": "object", + "required": [ + "statusCode", + "error", + "message" + ], + "properties": { + "statusCode": { + "type": "number", + "example": 400 + }, + "error": { + "type": "string", + "example": "Bad Request" + }, + "message": { + "type": "string" + } + } + }, + "404_response": { + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Not Found", + "enum": [ + "Not Found" + ] + }, + "message": { + "type": "string", + "example": "Saved object [index-pattern/caaad6d0-920c-11ed-b36a-874bd1548a00] not found" + }, + "statusCode": { + "type": "integer", + "example": 404, + "enum": [ + 404 + ] + } + } + }, + "update_data_view_request_object": { + "title": "Update data view request", + "type": "object", + "required": [ + "data_view" + ], + "properties": { + "data_view": { + "type": "object", + "description": "The data view properties you want to update. Only the specified properties are updated in the data view. Unspecified fields stay as they are persisted.\n", + "properties": { + "allowNoIndex": { + "$ref": "#/components/schemas/allownoindex" + }, + "fieldFormats": { + "$ref": "#/components/schemas/fieldformats" + }, + "fields": { + "type": "object" + }, + "name": { + "type": "string" + }, + "runtimeFieldMap": { + "$ref": "#/components/schemas/runtimefieldmap" + }, + "sourceFilters": { + "$ref": "#/components/schemas/sourcefilters" + }, + "timeFieldName": { + "$ref": "#/components/schemas/timefieldname" + }, + "title": { + "$ref": "#/components/schemas/title" + }, + "type": { + "$ref": "#/components/schemas/type" + }, + "typeMeta": { + "$ref": "#/components/schemas/typemeta" + } + } + }, + "refresh_fields": { + "type": "boolean", + "description": "Reloads the data view fields after the data view is updated.", + "default": false + } + } + } + } + } +} \ No newline at end of file diff --git a/src/plugins/data_views/docs/openapi/bundled.yaml b/src/plugins/data_views/docs/openapi/bundled.yaml new file mode 100644 index 0000000000000..b1c00562cef06 --- /dev/null +++ b/src/plugins/data_views/docs/openapi/bundled.yaml @@ -0,0 +1,1969 @@ +openapi: 3.1.0 +info: + title: Data views + description: OpenAPI schema for data view endpoints + version: '0.1' + contact: + name: Kibana Core Team + license: + name: Elastic License 2.0 + url: https://www.elastic.co/licensing/elastic-license +servers: + - url: http://localhost:5601 + description: local +security: + - basicAuth: [] + - apiKeyAuth: [] +tags: + - name: data views + description: Data view APIs enable you to manage data views, formerly known as Kibana index patterns. +paths: + /api/data_views: + get: + summary: Retrieves a list of all data views. + operationId: getAllDataViews + description: | + This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. + tags: + - data views + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: object + properties: + data_view: + type: array + items: + type: object + properties: + id: + type: string + name: + type: string + namespaces: + type: array + items: + type: string + title: + type: string + typeMeta: + type: object + examples: + getAllDataViewsResponse: + $ref: '#/components/examples/get_data_views_response' + servers: + - url: https://localhost:5601 + /api/data_views/data_view: + post: + summary: Creates a data view. + operationId: createDataView + description: | + This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. + tags: + - data views + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/create_data_view_request_object' + examples: + createDataViewRequest: + $ref: '#/components/examples/create_data_view_request' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '#/components/schemas/data_view_response_object' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/400_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /api/data_views/data_view/{viewId}: + get: + summary: Retrieves a single data view by identifier. + operationId: getDataView + description: | + This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. + tags: + - data views + parameters: + - $ref: '#/components/parameters/view_id' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '#/components/schemas/data_view_response_object' + examples: + getDataViewResponse: + $ref: '#/components/examples/get_data_view_response' + '404': + description: Object is not found. + content: + application/json: + schema: + $ref: '#/components/schemas/404_response' + delete: + summary: Deletes a data view. + operationId: deleteDataView + description: | + WARNING: When you delete a data view, it cannot be recovered. This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. + tags: + - data views + parameters: + - $ref: '#/components/parameters/view_id' + responses: + '204': + description: Indicates a successful call. + '404': + description: Object is not found. + content: + application/json: + schema: + $ref: '#/components/schemas/404_response' + servers: + - url: https://localhost:5601 + post: + summary: Updates a data view. + operationId: updateDataView + description: | + This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. + tags: + - data views + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/view_id' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/update_data_view_request_object' + examples: + updateDataViewRequest: + $ref: '#/components/examples/update_data_view_request' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '#/components/schemas/data_view_response_object' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/400_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /api/data_views/data_view/{viewId}/runtime_field/{fieldName}: + get: + summary: Retrieves a runtime field. + operationId: getRuntimeField + description: | + This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. + tags: + - data views + parameters: + - $ref: '#/components/parameters/field_name' + - $ref: '#/components/parameters/view_id' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: object + properties: + data_view: + type: object + fields: + type: array + items: + type: object + examples: + getRuntimeFieldResponse: + $ref: '#/components/examples/get_runtime_field_response' + '404': + description: Object is not found. + content: + application/json: + schema: + $ref: '#/components/schemas/404_response' + servers: + - url: https://localhost:5601 + /api/data_views/default: + get: + summary: Retrieves the default data view identifier. + operationId: getDefaultDataView + description: | + This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. + tags: + - data views + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: object + properties: + data_view_id: + type: string + examples: + getDefaultDataViewResponse: + $ref: '#/components/examples/get_default_data_view_response' + post: + summary: Sets the default data view identifier. + operationId: setDefaultDatailView + description: | + This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. + tags: + - data views + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - data_view_id + properties: + data_view_id: + type: + - string + - 'null' + description: | + The data view identifier. NOTE: The API does not validate whether it is a valid identifier. Use `null` to unset the default data view. + force: + type: boolean + description: Update an existing default data view identifier. + default: false + examples: + setDefaultDataViewRequest: + $ref: '#/components/examples/set_default_data_view_request' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: object + properties: + acknowledged: + type: boolean + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + apiKeyAuth: + type: apiKey + in: header + name: ApiKey + examples: + get_data_views_response: + summary: The get all data views API returns a list of data views. + value: + data_view: + - id: ff959d40-b880-11e8-a6d9-e546fe2bba5f + namespaces: + - default + title: kibana_sample_data_ecommerce + typeMeta: {} + name: Kibana Sample Data eCommerce + - id: d3d7af60-4c81-11e8-b3d7-01146121b73d + namespaces: + - default + title: kibana_sample_data_flights + name: Kibana Sample Data Flights + - id: 90943e30-9a47-11e8-b64d-95841ca0b247 + namespaces: + - default + title: kibana_sample_data_logs + name: Kibana Sample Data Logs + create_data_view_request: + summary: Create a data view with runtime fields. + value: + data_view: + title: logstash-* + name: My Logstash data view + runtimeFieldMap: + runtime_shape_name: + type: keyword + script: + source: emit(doc['shape_name'].value) + get_data_view_response: + summary: The get data view API returns a JSON object that contains information about the data view. + value: + data_view: + id: ff959d40-b880-11e8-a6d9-e546fe2bba5f + version: WzUsMV0= + title: kibana_sample_data_ecommerce + timeFieldName: order_date + sourceFilters: [] + fields: + _id: + count: 0 + name: _id + type: string + esTypes: + - _id + scripted: false + searchable: true + aggregatable: false + readFromDocValues: false + format: + id: string + shortDotsEnable: false + isMapped: true + _index: + count: 0 + name: _index + type: string + esTypes: + - _index + scripted: false + searchable: true + aggregatable: true + readFromDocValues: false + format: + id: string + shortDotsEnable: false + isMapped: true + _score: + count: 0 + name: _score + type: number + scripted: false + searchable: false + aggregatable: false + readFromDocValues: false + format: + id: number + shortDotsEnable: false + isMapped: true + _source: + count: 0 + name: _source + type: _source + esTypes: + - _source + scripted: false + searchable: false + aggregatable: false + readFromDocValues: false + format: + id: _source + shortDotsEnable: false + isMapped: true + category: + count: 0 + name: category + type: string + esTypes: + - text + scripted: false + searchable: true + aggregatable: false + readFromDocValues: false + format: + id: string + shortDotsEnable: false + isMapped: true + category.keyword: + count: 0 + name: category.keyword + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + subType: + multi: + parent: category + format: + id: string + shortDotsEnable: false + isMapped: true + currency: + count: 0 + name: currency + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + customer_birth_date: + count: 0 + name: customer_birth_date + type: date + esTypes: + - date + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: date + shortDotsEnable: false + isMapped: true + customer_first_name: + count: 0 + name: customer_first_name + type: string + esTypes: + - text + scripted: false + searchable: true + aggregatable: false + readFromDocValues: false + format: + id: string + shortDotsEnable: false + isMapped: true + customer_first_name.keyword: + count: 0 + name: customer_first_name.keyword + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + subType: + multi: + parent: customer_first_name + format: + id: string + shortDotsEnable: false + isMapped: true + customer_full_name: + count: 0 + name: customer_full_name + type: string + esTypes: + - text + scripted: false + searchable: true + aggregatable: false + readFromDocValues: false + format: + id: string + shortDotsEnable: false + isMapped: true + customer_full_name.keyword: + count: 0 + name: customer_full_name.keyword + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + subType: + multi: + parent: customer_full_name + format: + id: string + shortDotsEnable: false + isMapped: true + customer_gender: + count: 0 + name: customer_gender + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + customer_id: + count: 0 + name: customer_id + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + customer_last_name: + count: 0 + name: customer_last_name + type: string + esTypes: + - text + scripted: false + searchable: true + aggregatable: false + readFromDocValues: false + format: + id: string + shortDotsEnable: false + isMapped: true + customer_last_name.keyword: + count: 0 + name: customer_last_name.keyword + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + subType: + multi: + parent: customer_last_name + format: + id: string + shortDotsEnable: false + isMapped: true + customer_phone: + count: 0 + name: customer_phone + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + day_of_week: + count: 0 + name: day_of_week + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + day_of_week_i: + count: 0 + name: day_of_week_i + type: number + esTypes: + - integer + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: number + shortDotsEnable: false + isMapped: true + email: + count: 0 + name: email + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + event.dataset: + count: 0 + name: event.dataset + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + geoip.city_name: + count: 0 + name: geoip.city_name + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + geoip.continent_name: + count: 0 + name: geoip.continent_name + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + geoip.country_iso_code: + count: 0 + name: geoip.country_iso_code + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + geoip.location: + count: 0 + name: geoip.location + type: geo_point + esTypes: + - geo_point + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: geo_point + params: + transform: wkt + shortDotsEnable: false + isMapped: true + geoip.region_name: + count: 0 + name: geoip.region_name + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + manufacturer: + count: 0 + name: manufacturer + type: string + esTypes: + - text + scripted: false + searchable: true + aggregatable: false + readFromDocValues: false + format: + id: string + shortDotsEnable: false + isMapped: true + manufacturer.keyword: + count: 0 + name: manufacturer.keyword + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + subType: + multi: + parent: manufacturer + format: + id: string + shortDotsEnable: false + isMapped: true + order_date: + count: 0 + name: order_date + type: date + esTypes: + - date + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: date + shortDotsEnable: false + isMapped: true + order_id: + count: 0 + name: order_id + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + products._id: + count: 0 + name: products._id + type: string + esTypes: + - text + scripted: false + searchable: true + aggregatable: false + readFromDocValues: false + format: + id: string + shortDotsEnable: false + isMapped: true + products._id.keyword: + count: 0 + name: products._id.keyword + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + subType: + multi: + parent: products._id + format: + id: string + shortDotsEnable: false + isMapped: true + products.base_price: + count: 0 + name: products.base_price + type: number + esTypes: + - half_float + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: number + params: + pattern: $0,0.00 + shortDotsEnable: false + isMapped: true + products.base_unit_price: + count: 0 + name: products.base_unit_price + type: number + esTypes: + - half_float + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: number + params: + pattern: $0,0.00 + shortDotsEnable: false + isMapped: true + products.category: + count: 0 + name: products.category + type: string + esTypes: + - text + scripted: false + searchable: true + aggregatable: false + readFromDocValues: false + format: + id: string + shortDotsEnable: false + isMapped: true + products.category.keyword: + count: 0 + name: products.category.keyword + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + subType: + multi: + parent: products.category + format: + id: string + shortDotsEnable: false + isMapped: true + products.created_on: + count: 0 + name: products.created_on + type: date + esTypes: + - date + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: date + shortDotsEnable: false + isMapped: true + products.discount_amount: + count: 0 + name: products.discount_amount + type: number + esTypes: + - half_float + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: number + shortDotsEnable: false + isMapped: true + products.discount_percentage: + count: 0 + name: products.discount_percentage + type: number + esTypes: + - half_float + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: number + shortDotsEnable: false + isMapped: true + products.manufacturer: + count: 1 + name: products.manufacturer + type: string + esTypes: + - text + scripted: false + searchable: true + aggregatable: false + readFromDocValues: false + format: + id: string + shortDotsEnable: false + isMapped: true + products.manufacturer.keyword: + count: 0 + name: products.manufacturer.keyword + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + subType: + multi: + parent: products.manufacturer + format: + id: string + shortDotsEnable: false + isMapped: true + products.min_price: + count: 0 + name: products.min_price + type: number + esTypes: + - half_float + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: number + params: + pattern: $0,0.00 + shortDotsEnable: false + isMapped: true + products.price: + count: 1 + name: products.price + type: number + esTypes: + - half_float + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: number + params: + pattern: $0,0.00 + shortDotsEnable: false + isMapped: true + products.product_id: + count: 0 + name: products.product_id + type: number + esTypes: + - long + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: number + shortDotsEnable: false + isMapped: true + products.product_name: + count: 1 + name: products.product_name + type: string + esTypes: + - text + scripted: false + searchable: true + aggregatable: false + readFromDocValues: false + format: + id: string + shortDotsEnable: false + isMapped: true + products.product_name.keyword: + count: 0 + name: products.product_name.keyword + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + subType: + multi: + parent: products.product_name + format: + id: string + shortDotsEnable: false + isMapped: true + products.quantity: + count: 0 + name: products.quantity + type: number + esTypes: + - integer + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: number + shortDotsEnable: false + isMapped: true + products.sku: + count: 0 + name: products.sku + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + products.tax_amount: + count: 0 + name: products.tax_amount + type: number + esTypes: + - half_float + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: number + shortDotsEnable: false + isMapped: true + products.taxful_price: + count: 0 + name: products.taxful_price + type: number + esTypes: + - half_float + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: number + params: + pattern: $0,0.00 + shortDotsEnable: false + isMapped: true + products.taxless_price: + count: 0 + name: products.taxless_price + type: number + esTypes: + - half_float + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: number + params: + pattern: $0,0.00 + shortDotsEnable: false + isMapped: true + products.unit_discount_amount: + count: 0 + name: products.unit_discount_amount + type: number + esTypes: + - half_float + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: number + shortDotsEnable: false + isMapped: true + sku: + count: 0 + name: sku + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + taxful_total_price: + count: 0 + name: taxful_total_price + type: number + esTypes: + - half_float + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: number + params: + pattern: $0,0.[00] + shortDotsEnable: false + isMapped: true + taxless_total_price: + count: 0 + name: taxless_total_price + type: number + esTypes: + - half_float + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: number + params: + pattern: $0,0.00 + shortDotsEnable: false + isMapped: true + total_quantity: + count: 1 + name: total_quantity + type: number + esTypes: + - integer + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: number + shortDotsEnable: false + isMapped: true + total_unique_products: + count: 0 + name: total_unique_products + type: number + esTypes: + - integer + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: number + shortDotsEnable: false + isMapped: true + type: + count: 0 + name: type + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + user: + count: 0 + name: user + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + typeMeta: {} + fieldFormats: + taxful_total_price: + id: number + params: + pattern: $0,0.[00] + products.price: + id: number + params: + pattern: $0,0.00 + taxless_total_price: + id: number + params: + pattern: $0,0.00 + products.taxless_price: + id: number + params: + pattern: $0,0.00 + products.taxful_price: + id: number + params: + pattern: $0,0.00 + products.min_price: + id: number + params: + pattern: $0,0.00 + products.base_unit_price: + id: number + params: + pattern: $0,0.00 + products.base_price: + id: number + params: + pattern: $0,0.00 + runtimeFieldMap: {} + fieldAttrs: + products.manufacturer: + count: 1 + products.price: + count: 1 + products.product_name: + count: 1 + total_quantity: + count: 1 + allowNoIndex: false + name: Kibana Sample Data eCommerce + namespaces: + - default + update_data_view_request: + summary: Update some properties for a data view. + value: + data_view: + title: kibana_sample_data_ecommerce + timeFieldName: order_date + allowNoIndex: false + name: Kibana Sample Data eCommerce + refresh_fields: true + get_runtime_field_response: + summary: The get runtime field API returns a JSON object that contains information about the runtime field (`hour_of_day`) and the data view (`d3d7af60-4c81-11e8-b3d7-01146121b73d`). + value: + fields: + - count: 0 + name: hour_of_day + type: number + esTypes: + - long + scripted: false + searchable: true + aggregatable: true + readFromDocValues: false + shortDotsEnable: false + runtimeField: + type: long + script: + source: emit(doc['timestamp'].value.getHour()); + data_view: + id: d3d7af60-4c81-11e8-b3d7-01146121b73d + version: WzM2LDJd + title: kibana_sample_data_flights + timeFieldName: timestamp + sourceFilters: [] + fields: + hour_of_day: + count: 0 + name: hour_of_day + type: number + esTypes: + - long + scripted: false + searchable: true + aggregatable: true + readFromDocValues: false + format: + id: number + params: + pattern: '00' + shortDotsEnable: false + runtimeField: + type: long + script: + source: emit(doc['timestamp'].value.getHour()); + AvgTicketPrice: + count: 0 + name: AvgTicketPrice + type: number + esTypes: + - float + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: number + params: + pattern: $0,0.[00] + shortDotsEnable: false + isMapped: true + Cancelled: + count: 0 + name: Cancelled + type: boolean + esTypes: + - boolean + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: boolean + shortDotsEnable: false + isMapped: true + Carrier: + count: 0 + name: Carrier + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + Dest: + count: 0 + name: Dest + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + DestAirportID: + count: 0 + name: DestAirportID + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + DestCityName: + count: 0 + name: DestCityName + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + DestCountry: + count: 0 + name: DestCountry + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + DestLocation: + count: 0 + name: DestLocation + type: geo_point + esTypes: + - geo_point + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: geo_point + params: + transform: wkt + shortDotsEnable: false + isMapped: true + DestRegion: + count: 0 + name: DestRegion + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + DestWeather: + count: 0 + name: DestWeather + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + DistanceKilometers: + count: 0 + name: DistanceKilometers + type: number + esTypes: + - float + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: number + shortDotsEnable: false + isMapped: true + DistanceMiles: + count: 0 + name: DistanceMiles + type: number + esTypes: + - float + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: number + shortDotsEnable: false + isMapped: true + FlightDelay: + count: 0 + name: FlightDelay + type: boolean + esTypes: + - boolean + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: boolean + shortDotsEnable: false + isMapped: true + FlightDelayMin: + count: 0 + name: FlightDelayMin + type: number + esTypes: + - integer + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: number + shortDotsEnable: false + isMapped: true + FlightDelayType: + count: 0 + name: FlightDelayType + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + FlightNum: + count: 0 + name: FlightNum + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + FlightTimeHour: + count: 0 + name: FlightTimeHour + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + FlightTimeMin: + count: 0 + name: FlightTimeMin + type: number + esTypes: + - float + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: number + shortDotsEnable: false + isMapped: true + Origin: + count: 0 + name: Origin + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + OriginAirportID: + count: 0 + name: OriginAirportID + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + OriginCityName: + count: 0 + name: OriginCityName + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + OriginCountry: + count: 0 + name: OriginCountry + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + OriginLocation: + count: 0 + name: OriginLocation + type: geo_point + esTypes: + - geo_point + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: geo_point + params: + transform: wkt + shortDotsEnable: false + isMapped: true + OriginRegion: + count: 0 + name: OriginRegion + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + OriginWeather: + count: 0 + name: OriginWeather + type: string + esTypes: + - keyword + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: string + shortDotsEnable: false + isMapped: true + _id: + count: 0 + name: _id + type: string + esTypes: + - _id + scripted: false + searchable: true + aggregatable: false + readFromDocValues: false + format: + id: string + shortDotsEnable: false + isMapped: true + _index: + count: 0 + name: _index + type: string + esTypes: + - _index + scripted: false + searchable: true + aggregatable: true + readFromDocValues: false + format: + id: string + shortDotsEnable: false + isMapped: true + _score: + count: 0 + name: _score + type: number + scripted: false + searchable: false + aggregatable: false + readFromDocValues: false + format: + id: number + shortDotsEnable: false + isMapped: true + _source: + count: 0 + name: _source + type: _source + esTypes: + - _source + scripted: false + searchable: false + aggregatable: false + readFromDocValues: false + format: + id: _source + shortDotsEnable: false + isMapped: true + dayOfWeek: + count: 0 + name: dayOfWeek + type: number + esTypes: + - integer + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: number + shortDotsEnable: false + isMapped: true + timestamp: + count: 0 + name: timestamp + type: date + esTypes: + - date + scripted: false + searchable: true + aggregatable: true + readFromDocValues: true + format: + id: date + shortDotsEnable: false + isMapped: true + fieldFormats: + hour_of_day: + id: number + params: + pattern: '00' + AvgTicketPrice: + id: number + params: + pattern: $0,0.[00] + runtimeFieldMap: + hour_of_day: + type: long + script: + source: emit(doc['timestamp'].value.getHour()); + fieldAttrs: {} + allowNoIndex: false + name: Kibana Sample Data Flights + get_default_data_view_response: + summary: The get default data view API returns the default data view identifier. + value: + data_view_id: ff959d40-b880-11e8-a6d9-e546fe2bba5f + set_default_data_view_request: + summary: Set the default data view identifier. + value: + data_view_id: ff959d40-b880-11e8-a6d9-e546fe2bba5f + force: true + parameters: + kbn_xsrf: + schema: + type: string + in: header + name: kbn-xsrf + description: Cross-site request forgery protection + required: true + view_id: + in: path + name: viewId + description: An identifier for the data view. + required: true + schema: + type: string + example: ff959d40-b880-11e8-a6d9-e546fe2bba5f + field_name: + in: path + name: fieldName + description: The name of the runtime field. + required: true + schema: + type: string + example: hour_of_day + schemas: + allownoindex: + type: boolean + description: Allows the data view saved object to exist before the data is available. + fieldattrs: + type: object + description: A map of field attributes by field name. + fieldformats: + type: object + description: A map of field formats by field name. + namespaces: + type: array + description: An array of space identifiers for sharing the data view between multiple spaces. + items: + type: string + default: default + runtimefieldmap: + type: object + description: A map of runtime field definitions by field name. + sourcefilters: + type: array + description: The array of field names you want to filter out in Discover. + timefieldname: + type: string + description: The timestamp field name, which you use for time-based data views. + title: + type: string + description: Comma-separated list of data streams, indices, and aliases that you want to search. Supports wildcards (`*`). + type: + type: string + description: When set to `rollup`, identifies the rollup data views. + typemeta: + type: object + description: When you use rollup indices, contains the field list for the rollup data view API endpoints. + create_data_view_request_object: + title: Create data view request + type: object + required: + - data_view + properties: + data_view: + type: object + required: + - title + description: The data view object. + properties: + allowNoIndex: + $ref: '#/components/schemas/allownoindex' + fieldAttrs: + $ref: '#/components/schemas/fieldattrs' + fieldFormats: + $ref: '#/components/schemas/fieldformats' + fields: + type: object + id: + type: string + name: + type: string + description: The data view name. + namespaces: + $ref: '#/components/schemas/namespaces' + runtimeFieldMap: + $ref: '#/components/schemas/runtimefieldmap' + sourceFilters: + $ref: '#/components/schemas/sourcefilters' + timeFieldName: + $ref: '#/components/schemas/timefieldname' + title: + $ref: '#/components/schemas/title' + type: + $ref: '#/components/schemas/type' + typeMeta: + $ref: '#/components/schemas/typemeta' + version: + type: string + override: + type: boolean + description: Override an existing data view if a data view with the provided title already exists. + default: false + data_view_response_object: + title: Data view response properties + type: object + properties: + data_view: + type: object + properties: + allowNoIndex: + $ref: '#/components/schemas/allownoindex' + fieldAttrs: + $ref: '#/components/schemas/fieldattrs' + fieldFormats: + $ref: '#/components/schemas/fieldformats' + fields: + type: object + id: + type: string + example: ff959d40-b880-11e8-a6d9-e546fe2bba5f + name: + type: string + description: The data view name. + namespaces: + $ref: '#/components/schemas/namespaces' + runtimeFieldMap: + $ref: '#/components/schemas/runtimefieldmap' + sourceFilters: + $ref: '#/components/schemas/sourcefilters' + timeFieldName: + $ref: '#/components/schemas/timefieldname' + title: + $ref: '#/components/schemas/title' + typeMeta: + $ref: '#/components/schemas/typemeta' + version: + type: string + example: WzQ2LDJd + 400_response: + title: Bad request + type: object + required: + - statusCode + - error + - message + properties: + statusCode: + type: number + example: 400 + error: + type: string + example: Bad Request + message: + type: string + 404_response: + type: object + properties: + error: + type: string + example: Not Found + enum: + - Not Found + message: + type: string + example: Saved object [index-pattern/caaad6d0-920c-11ed-b36a-874bd1548a00] not found + statusCode: + type: integer + example: 404 + enum: + - 404 + update_data_view_request_object: + title: Update data view request + type: object + required: + - data_view + properties: + data_view: + type: object + description: | + The data view properties you want to update. Only the specified properties are updated in the data view. Unspecified fields stay as they are persisted. + properties: + allowNoIndex: + $ref: '#/components/schemas/allownoindex' + fieldFormats: + $ref: '#/components/schemas/fieldformats' + fields: + type: object + name: + type: string + runtimeFieldMap: + $ref: '#/components/schemas/runtimefieldmap' + sourceFilters: + $ref: '#/components/schemas/sourcefilters' + timeFieldName: + $ref: '#/components/schemas/timefieldname' + title: + $ref: '#/components/schemas/title' + type: + $ref: '#/components/schemas/type' + typeMeta: + $ref: '#/components/schemas/typemeta' + refresh_fields: + type: boolean + description: Reloads the data view fields after the data view is updated. + default: false diff --git a/src/plugins/data_views/docs/openapi/components/examples/create_data_view_request.yaml b/src/plugins/data_views/docs/openapi/components/examples/create_data_view_request.yaml new file mode 100644 index 0000000000000..82752970acd30 --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/examples/create_data_view_request.yaml @@ -0,0 +1,16 @@ +summary: Create a data view with runtime fields. +value: + { + "data_view": { + "title": "logstash-*", + "name": "My Logstash data view", + "runtimeFieldMap": { + "runtime_shape_name": { + "type": "keyword", + "script": { + "source": "emit(doc['shape_name'].value)" + } + } + } + } + } \ No newline at end of file diff --git a/src/plugins/data_views/docs/openapi/components/examples/create_data_view_response.yaml b/src/plugins/data_views/docs/openapi/components/examples/create_data_view_response.yaml new file mode 100644 index 0000000000000..623f1855feee6 --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/examples/create_data_view_response.yaml @@ -0,0 +1,50 @@ +summary: The create data view API returns a JSON object that contains details about the new data view. +value: + { + "data_view": { + "id": "b561acfb-0181-455e-84a3-ce8980b2272f", + "version": "WzQ5LDJd", + "title": "logstash-*", + "sourceFilters": [], + "fields": { + "runtime_shape_name": { + "count": 0, + "name": "runtime_shape_name", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "runtimeField": { + "type": "keyword", + "script": { + "source": "emit(doc['shape_name'].value)" + } + } + } + }, + "typeMeta": {}, + "fieldFormats": {}, + "runtimeFieldMap": { + "runtime_shape_name": { + "type": "keyword", + "script": { + "source": "emit(doc['shape_name'].value)" + } + } + }, + "fieldAttrs": {}, + "allowNoIndex": false, + "name": "My Logstash data view", + "namespaces": [ + "default" + ] + } + } \ No newline at end of file diff --git a/src/plugins/data_views/docs/openapi/components/examples/get_data_view_response.yaml b/src/plugins/data_views/docs/openapi/components/examples/get_data_view_response.yaml new file mode 100644 index 0000000000000..5a731f4789138 --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/examples/get_data_view_response.yaml @@ -0,0 +1,1156 @@ +summary: The get data view API returns a JSON object that contains information about the data view. +value: + { + "data_view": { + "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", + "version": "WzUsMV0=", + "title": "kibana_sample_data_ecommerce", + "timeFieldName": "order_date", + "sourceFilters": [], + "fields": { + "_id": { + "count": 0, + "name": "_id", + "type": "string", + "esTypes": [ + "_id" + ], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "_index": { + "count": 0, + "name": "_index", + "type": "string", + "esTypes": [ + "_index" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "_score": { + "count": 0, + "name": "_score", + "type": "number", + "scripted": false, + "searchable": false, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "_source": { + "count": 0, + "name": "_source", + "type": "_source", + "esTypes": [ + "_source" + ], + "scripted": false, + "searchable": false, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "_source" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "category": { + "count": 0, + "name": "category", + "type": "string", + "esTypes": [ + "text" + ], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "category.keyword": { + "count": 0, + "name": "category.keyword", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "subType": { + "multi": { + "parent": "category" + } + }, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "currency": { + "count": 0, + "name": "currency", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "customer_birth_date": { + "count": 0, + "name": "customer_birth_date", + "type": "date", + "esTypes": [ + "date" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "date" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "customer_first_name": { + "count": 0, + "name": "customer_first_name", + "type": "string", + "esTypes": [ + "text" + ], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "customer_first_name.keyword": { + "count": 0, + "name": "customer_first_name.keyword", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "subType": { + "multi": { + "parent": "customer_first_name" + } + }, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "customer_full_name": { + "count": 0, + "name": "customer_full_name", + "type": "string", + "esTypes": [ + "text" + ], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "customer_full_name.keyword": { + "count": 0, + "name": "customer_full_name.keyword", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "subType": { + "multi": { + "parent": "customer_full_name" + } + }, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "customer_gender": { + "count": 0, + "name": "customer_gender", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "customer_id": { + "count": 0, + "name": "customer_id", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "customer_last_name": { + "count": 0, + "name": "customer_last_name", + "type": "string", + "esTypes": [ + "text" + ], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "customer_last_name.keyword": { + "count": 0, + "name": "customer_last_name.keyword", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "subType": { + "multi": { + "parent": "customer_last_name" + } + }, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "customer_phone": { + "count": 0, + "name": "customer_phone", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "day_of_week": { + "count": 0, + "name": "day_of_week", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "day_of_week_i": { + "count": 0, + "name": "day_of_week_i", + "type": "number", + "esTypes": [ + "integer" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "email": { + "count": 0, + "name": "email", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "event.dataset": { + "count": 0, + "name": "event.dataset", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "geoip.city_name": { + "count": 0, + "name": "geoip.city_name", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "geoip.continent_name": { + "count": 0, + "name": "geoip.continent_name", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "geoip.country_iso_code": { + "count": 0, + "name": "geoip.country_iso_code", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "geoip.location": { + "count": 0, + "name": "geoip.location", + "type": "geo_point", + "esTypes": [ + "geo_point" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "geo_point", + "params": { + "transform": "wkt" + } + }, + "shortDotsEnable": false, + "isMapped": true + }, + "geoip.region_name": { + "count": 0, + "name": "geoip.region_name", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "manufacturer": { + "count": 0, + "name": "manufacturer", + "type": "string", + "esTypes": [ + "text" + ], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "manufacturer.keyword": { + "count": 0, + "name": "manufacturer.keyword", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "subType": { + "multi": { + "parent": "manufacturer" + } + }, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "order_date": { + "count": 0, + "name": "order_date", + "type": "date", + "esTypes": [ + "date" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "date" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "order_id": { + "count": 0, + "name": "order_id", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products._id": { + "count": 0, + "name": "products._id", + "type": "string", + "esTypes": [ + "text" + ], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products._id.keyword": { + "count": 0, + "name": "products._id.keyword", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "subType": { + "multi": { + "parent": "products._id" + } + }, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.base_price": { + "count": 0, + "name": "products.base_price", + "type": "number", + "esTypes": [ + "half_float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.base_unit_price": { + "count": 0, + "name": "products.base_unit_price", + "type": "number", + "esTypes": [ + "half_float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.category": { + "count": 0, + "name": "products.category", + "type": "string", + "esTypes": [ + "text" + ], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.category.keyword": { + "count": 0, + "name": "products.category.keyword", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "subType": { + "multi": { + "parent": "products.category" + } + }, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.created_on": { + "count": 0, + "name": "products.created_on", + "type": "date", + "esTypes": [ + "date" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "date" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.discount_amount": { + "count": 0, + "name": "products.discount_amount", + "type": "number", + "esTypes": [ + "half_float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.discount_percentage": { + "count": 0, + "name": "products.discount_percentage", + "type": "number", + "esTypes": [ + "half_float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.manufacturer": { + "count": 1, + "name": "products.manufacturer", + "type": "string", + "esTypes": [ + "text" + ], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.manufacturer.keyword": { + "count": 0, + "name": "products.manufacturer.keyword", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "subType": { + "multi": { + "parent": "products.manufacturer" + } + }, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.min_price": { + "count": 0, + "name": "products.min_price", + "type": "number", + "esTypes": [ + "half_float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.price": { + "count": 1, + "name": "products.price", + "type": "number", + "esTypes": [ + "half_float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.product_id": { + "count": 0, + "name": "products.product_id", + "type": "number", + "esTypes": [ + "long" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.product_name": { + "count": 1, + "name": "products.product_name", + "type": "string", + "esTypes": [ + "text" + ], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.product_name.keyword": { + "count": 0, + "name": "products.product_name.keyword", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "subType": { + "multi": { + "parent": "products.product_name" + } + }, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.quantity": { + "count": 0, + "name": "products.quantity", + "type": "number", + "esTypes": [ + "integer" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.sku": { + "count": 0, + "name": "products.sku", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.tax_amount": { + "count": 0, + "name": "products.tax_amount", + "type": "number", + "esTypes": [ + "half_float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.taxful_price": { + "count": 0, + "name": "products.taxful_price", + "type": "number", + "esTypes": [ + "half_float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.taxless_price": { + "count": 0, + "name": "products.taxless_price", + "type": "number", + "esTypes": [ + "half_float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "shortDotsEnable": false, + "isMapped": true + }, + "products.unit_discount_amount": { + "count": 0, + "name": "products.unit_discount_amount", + "type": "number", + "esTypes": [ + "half_float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "sku": { + "count": 0, + "name": "sku", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "taxful_total_price": { + "count": 0, + "name": "taxful_total_price", + "type": "number", + "esTypes": [ + "half_float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.[00]" + } + }, + "shortDotsEnable": false, + "isMapped": true + }, + "taxless_total_price": { + "count": 0, + "name": "taxless_total_price", + "type": "number", + "esTypes": [ + "half_float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "shortDotsEnable": false, + "isMapped": true + }, + "total_quantity": { + "count": 1, + "name": "total_quantity", + "type": "number", + "esTypes": [ + "integer" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "total_unique_products": { + "count": 0, + "name": "total_unique_products", + "type": "number", + "esTypes": [ + "integer" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "type": { + "count": 0, + "name": "type", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "user": { + "count": 0, + "name": "user", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + } + }, + "typeMeta": {}, + "fieldFormats": { + "taxful_total_price": { + "id": "number", + "params": { + "pattern": "$0,0.[00]" + } + }, + "products.price": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "taxless_total_price": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "products.taxless_price": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "products.taxful_price": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "products.min_price": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "products.base_unit_price": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + }, + "products.base_price": { + "id": "number", + "params": { + "pattern": "$0,0.00" + } + } + }, + "runtimeFieldMap": {}, + "fieldAttrs": { + "products.manufacturer": { + "count": 1 + }, + "products.price": { + "count": 1 + }, + "products.product_name": { + "count": 1 + }, + "total_quantity": { + "count": 1 + } + }, + "allowNoIndex": false, + "name": "Kibana Sample Data eCommerce", + "namespaces": [ + "default" + ] + } + } \ No newline at end of file diff --git a/src/plugins/data_views/docs/openapi/components/examples/get_data_views_response.yaml b/src/plugins/data_views/docs/openapi/components/examples/get_data_views_response.yaml new file mode 100644 index 0000000000000..069cdc2bf6e58 --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/examples/get_data_views_response.yaml @@ -0,0 +1,31 @@ +summary: The get all data views API returns a list of data views. +value: + { + "data_view": [ + { + "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", + "namespaces": [ + "default" + ], + "title": "kibana_sample_data_ecommerce", + "typeMeta": {}, + "name": "Kibana Sample Data eCommerce" + }, + { + "id": "d3d7af60-4c81-11e8-b3d7-01146121b73d", + "namespaces": [ + "default" + ], + "title": "kibana_sample_data_flights", + "name": "Kibana Sample Data Flights" + }, + { + "id": "90943e30-9a47-11e8-b64d-95841ca0b247", + "namespaces": [ + "default" + ], + "title": "kibana_sample_data_logs", + "name": "Kibana Sample Data Logs" + } + ] + } diff --git a/src/plugins/data_views/docs/openapi/components/examples/get_default_data_view_response.yaml b/src/plugins/data_views/docs/openapi/components/examples/get_default_data_view_response.yaml new file mode 100644 index 0000000000000..7bc346b36924e --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/examples/get_default_data_view_response.yaml @@ -0,0 +1,5 @@ +summary: The get default data view API returns the default data view identifier. +value: + { + "data_view_id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f" + } \ No newline at end of file diff --git a/src/plugins/data_views/docs/openapi/components/examples/get_runtime_field_response.yaml b/src/plugins/data_views/docs/openapi/components/examples/get_runtime_field_response.yaml new file mode 100644 index 0000000000000..2d743e6f2fff0 --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/examples/get_runtime_field_response.yaml @@ -0,0 +1,617 @@ +summary: The get runtime field API returns a JSON object that contains information about the runtime field (`hour_of_day`) and the data view (`d3d7af60-4c81-11e8-b3d7-01146121b73d`). +value: + { + "fields": [ + { + "count": 0, + "name": "hour_of_day", + "type": "number", + "esTypes": [ + "long" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": false, + "shortDotsEnable": false, + "runtimeField": { + "type": "long", + "script": { + "source": "emit(doc['timestamp'].value.getHour());" + } + } + } + ], + "data_view": { + "id": "d3d7af60-4c81-11e8-b3d7-01146121b73d", + "version": "WzM2LDJd", + "title": "kibana_sample_data_flights", + "timeFieldName": "timestamp", + "sourceFilters": [], + "fields": { + "hour_of_day": { + "count": 0, + "name": "hour_of_day", + "type": "number", + "esTypes": [ + "long" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": false, + "format": { + "id": "number", + "params": { + "pattern": "00" + } + }, + "shortDotsEnable": false, + "runtimeField": { + "type": "long", + "script": { + "source": "emit(doc['timestamp'].value.getHour());" + } + } + }, + "AvgTicketPrice": { + "count": 0, + "name": "AvgTicketPrice", + "type": "number", + "esTypes": [ + "float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.[00]" + } + }, + "shortDotsEnable": false, + "isMapped": true + }, + "Cancelled": { + "count": 0, + "name": "Cancelled", + "type": "boolean", + "esTypes": [ + "boolean" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "boolean" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "Carrier": { + "count": 0, + "name": "Carrier", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "Dest": { + "count": 0, + "name": "Dest", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "DestAirportID": { + "count": 0, + "name": "DestAirportID", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "DestCityName": { + "count": 0, + "name": "DestCityName", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "DestCountry": { + "count": 0, + "name": "DestCountry", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "DestLocation": { + "count": 0, + "name": "DestLocation", + "type": "geo_point", + "esTypes": [ + "geo_point" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "geo_point", + "params": { + "transform": "wkt" + } + }, + "shortDotsEnable": false, + "isMapped": true + }, + "DestRegion": { + "count": 0, + "name": "DestRegion", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "DestWeather": { + "count": 0, + "name": "DestWeather", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "DistanceKilometers": { + "count": 0, + "name": "DistanceKilometers", + "type": "number", + "esTypes": [ + "float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "DistanceMiles": { + "count": 0, + "name": "DistanceMiles", + "type": "number", + "esTypes": [ + "float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "FlightDelay": { + "count": 0, + "name": "FlightDelay", + "type": "boolean", + "esTypes": [ + "boolean" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "boolean" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "FlightDelayMin": { + "count": 0, + "name": "FlightDelayMin", + "type": "number", + "esTypes": [ + "integer" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "FlightDelayType": { + "count": 0, + "name": "FlightDelayType", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "FlightNum": { + "count": 0, + "name": "FlightNum", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "FlightTimeHour": { + "count": 0, + "name": "FlightTimeHour", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "FlightTimeMin": { + "count": 0, + "name": "FlightTimeMin", + "type": "number", + "esTypes": [ + "float" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "Origin": { + "count": 0, + "name": "Origin", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "OriginAirportID": { + "count": 0, + "name": "OriginAirportID", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "OriginCityName": { + "count": 0, + "name": "OriginCityName", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "OriginCountry": { + "count": 0, + "name": "OriginCountry", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "OriginLocation": { + "count": 0, + "name": "OriginLocation", + "type": "geo_point", + "esTypes": [ + "geo_point" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "geo_point", + "params": { + "transform": "wkt" + } + }, + "shortDotsEnable": false, + "isMapped": true + }, + "OriginRegion": { + "count": 0, + "name": "OriginRegion", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "OriginWeather": { + "count": 0, + "name": "OriginWeather", + "type": "string", + "esTypes": [ + "keyword" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "_id": { + "count": 0, + "name": "_id", + "type": "string", + "esTypes": [ + "_id" + ], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "_index": { + "count": 0, + "name": "_index", + "type": "string", + "esTypes": [ + "_index" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": false, + "format": { + "id": "string" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "_score": { + "count": 0, + "name": "_score", + "type": "number", + "scripted": false, + "searchable": false, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "_source": { + "count": 0, + "name": "_source", + "type": "_source", + "esTypes": [ + "_source" + ], + "scripted": false, + "searchable": false, + "aggregatable": false, + "readFromDocValues": false, + "format": { + "id": "_source" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "dayOfWeek": { + "count": 0, + "name": "dayOfWeek", + "type": "number", + "esTypes": [ + "integer" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "number" + }, + "shortDotsEnable": false, + "isMapped": true + }, + "timestamp": { + "count": 0, + "name": "timestamp", + "type": "date", + "esTypes": [ + "date" + ], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true, + "format": { + "id": "date" + }, + "shortDotsEnable": false, + "isMapped": true + } + }, + "fieldFormats": { + "hour_of_day": { + "id": "number", + "params": { + "pattern": "00" + } + }, + "AvgTicketPrice": { + "id": "number", + "params": { + "pattern": "$0,0.[00]" + } + } + }, + "runtimeFieldMap": { + "hour_of_day": { + "type": "long", + "script": { + "source": "emit(doc['timestamp'].value.getHour());" + } + } + }, + "fieldAttrs": {}, + "allowNoIndex": false, + "name": "Kibana Sample Data Flights" + } +} diff --git a/src/plugins/data_views/docs/openapi/components/examples/set_default_data_view_request.yaml b/src/plugins/data_views/docs/openapi/components/examples/set_default_data_view_request.yaml new file mode 100644 index 0000000000000..aaf49a4a503d3 --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/examples/set_default_data_view_request.yaml @@ -0,0 +1,6 @@ +summary: Set the default data view identifier. +value: + { + "data_view_id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", + "force": true + } \ No newline at end of file diff --git a/src/plugins/data_views/docs/openapi/components/examples/update_data_view_request.yaml b/src/plugins/data_views/docs/openapi/components/examples/update_data_view_request.yaml new file mode 100644 index 0000000000000..c56244b66caae --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/examples/update_data_view_request.yaml @@ -0,0 +1,11 @@ +summary: Update some properties for a data view. +value: + { + "data_view": { + "title": "kibana_sample_data_ecommerce", + "timeFieldName": "order_date", + "allowNoIndex": false, + "name": "Kibana Sample Data eCommerce" + }, + "refresh_fields": true +} \ No newline at end of file diff --git a/src/plugins/data_views/docs/openapi/components/headers/kbn_xsrf.yaml b/src/plugins/data_views/docs/openapi/components/headers/kbn_xsrf.yaml new file mode 100644 index 0000000000000..fe0402a43aa03 --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/headers/kbn_xsrf.yaml @@ -0,0 +1,6 @@ +schema: + type: string +in: header +name: kbn-xsrf +description: Cross-site request forgery protection +required: true diff --git a/src/plugins/data_views/docs/openapi/components/parameters/field_name.yaml b/src/plugins/data_views/docs/openapi/components/parameters/field_name.yaml new file mode 100644 index 0000000000000..1e0de3ec7c56d --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/parameters/field_name.yaml @@ -0,0 +1,7 @@ +in: path +name: fieldName +description: The name of the runtime field. +required: true +schema: + type: string + example: hour_of_day \ No newline at end of file diff --git a/src/plugins/data_views/docs/openapi/components/parameters/view_id.yaml b/src/plugins/data_views/docs/openapi/components/parameters/view_id.yaml new file mode 100644 index 0000000000000..6f75420fb618c --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/parameters/view_id.yaml @@ -0,0 +1,7 @@ +in: path +name: viewId +description: An identifier for the data view. +required: true +schema: + type: string + example: ff959d40-b880-11e8-a6d9-e546fe2bba5f \ No newline at end of file diff --git a/src/plugins/data_views/docs/openapi/components/schemas/400_response.yaml b/src/plugins/data_views/docs/openapi/components/schemas/400_response.yaml new file mode 100644 index 0000000000000..b251874008326 --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/schemas/400_response.yaml @@ -0,0 +1,15 @@ +title: Bad request +type: object +required: + - statusCode + - error + - message +properties: + statusCode: + type: number + example: 400 + error: + type: string + example: Bad Request + message: + type: string diff --git a/src/plugins/data_views/docs/openapi/components/schemas/404_response.yaml b/src/plugins/data_views/docs/openapi/components/schemas/404_response.yaml new file mode 100644 index 0000000000000..2cbd94660b8a7 --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/schemas/404_response.yaml @@ -0,0 +1,15 @@ +type: object +properties: + error: + type: string + example: Not Found + enum: + - Not Found + message: + type: string + example: "Saved object [index-pattern/caaad6d0-920c-11ed-b36a-874bd1548a00] not found" + statusCode: + type: integer + example: 404 + enum: + - 404 \ No newline at end of file diff --git a/src/plugins/data_views/docs/openapi/components/schemas/allownoindex.yaml b/src/plugins/data_views/docs/openapi/components/schemas/allownoindex.yaml new file mode 100644 index 0000000000000..15dacd74a4a93 --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/schemas/allownoindex.yaml @@ -0,0 +1,2 @@ +type: boolean +description: Allows the data view saved object to exist before the data is available. \ No newline at end of file diff --git a/src/plugins/data_views/docs/openapi/components/schemas/create_data_view_request_object.yaml b/src/plugins/data_views/docs/openapi/components/schemas/create_data_view_request_object.yaml new file mode 100644 index 0000000000000..ff2c34ed6d9ad --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/schemas/create_data_view_request_object.yaml @@ -0,0 +1,44 @@ +title: Create data view request +type: object +required: + - data_view +properties: + data_view: + type: object + required: + - title + description: The data view object. + properties: + allowNoIndex: + $ref: 'allownoindex.yaml' + fieldAttrs: + $ref: 'fieldattrs.yaml' + fieldFormats: + $ref: 'fieldformats.yaml' + fields: + type: object + id: + type: string + name: + type: string + description: The data view name. + namespaces: + $ref: 'namespaces.yaml' + runtimeFieldMap: + $ref: 'runtimefieldmap.yaml' + sourceFilters: + $ref: 'sourcefilters.yaml' + timeFieldName: + $ref: 'timefieldname.yaml' + title: + $ref: 'title.yaml' + type: + $ref: 'type.yaml' + typeMeta: + $ref: 'typemeta.yaml' + version: + type: string + override: + type: boolean + description: Override an existing data view if a data view with the provided title already exists. + default: false diff --git a/src/plugins/data_views/docs/openapi/components/schemas/data_view_response_object.yaml b/src/plugins/data_views/docs/openapi/components/schemas/data_view_response_object.yaml new file mode 100644 index 0000000000000..9d3c67201896b --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/schemas/data_view_response_object.yaml @@ -0,0 +1,36 @@ +title: Data view response properties +type: object +properties: + data_view: + type: object + properties: + allowNoIndex: + $ref: 'allownoindex.yaml' + fieldAttrs: + $ref: 'fieldattrs.yaml' + fieldFormats: + $ref: 'fieldformats.yaml' + fields: + type: object + id: + type: string + example: ff959d40-b880-11e8-a6d9-e546fe2bba5f + name: + type: string + description: The data view name. + namespaces: + $ref: 'namespaces.yaml' + runtimeFieldMap: + $ref: 'runtimefieldmap.yaml' + sourceFilters: + $ref: 'sourcefilters.yaml' + timeFieldName: + $ref: 'timefieldname.yaml' + title: + $ref: 'title.yaml' + typeMeta: + $ref: 'typemeta.yaml' + version: + type: string + example: WzQ2LDJd + \ No newline at end of file diff --git a/src/plugins/data_views/docs/openapi/components/schemas/fieldattrs.yaml b/src/plugins/data_views/docs/openapi/components/schemas/fieldattrs.yaml new file mode 100644 index 0000000000000..ede637d538a92 --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/schemas/fieldattrs.yaml @@ -0,0 +1,2 @@ +type: object +description: A map of field attributes by field name. \ No newline at end of file diff --git a/src/plugins/data_views/docs/openapi/components/schemas/fieldformats.yaml b/src/plugins/data_views/docs/openapi/components/schemas/fieldformats.yaml new file mode 100644 index 0000000000000..c0f4fdfa8588d --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/schemas/fieldformats.yaml @@ -0,0 +1,2 @@ +type: object +description: A map of field formats by field name. \ No newline at end of file diff --git a/src/plugins/data_views/docs/openapi/components/schemas/namespaces.yaml b/src/plugins/data_views/docs/openapi/components/schemas/namespaces.yaml new file mode 100644 index 0000000000000..5ed383aaa8c82 --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/schemas/namespaces.yaml @@ -0,0 +1,5 @@ +type: array +description: An array of space identifiers for sharing the data view between multiple spaces. +items: + type: string + default: default \ No newline at end of file diff --git a/src/plugins/data_views/docs/openapi/components/schemas/runtimefieldmap.yaml b/src/plugins/data_views/docs/openapi/components/schemas/runtimefieldmap.yaml new file mode 100644 index 0000000000000..96f34a08fe056 --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/schemas/runtimefieldmap.yaml @@ -0,0 +1,2 @@ +type: object +description: A map of runtime field definitions by field name. \ No newline at end of file diff --git a/src/plugins/data_views/docs/openapi/components/schemas/sourcefilters.yaml b/src/plugins/data_views/docs/openapi/components/schemas/sourcefilters.yaml new file mode 100644 index 0000000000000..4ba0980e43730 --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/schemas/sourcefilters.yaml @@ -0,0 +1,2 @@ +type: array +description: The array of field names you want to filter out in Discover. \ No newline at end of file diff --git a/src/plugins/data_views/docs/openapi/components/schemas/timefieldname.yaml b/src/plugins/data_views/docs/openapi/components/schemas/timefieldname.yaml new file mode 100644 index 0000000000000..97aa0f5070405 --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/schemas/timefieldname.yaml @@ -0,0 +1,2 @@ +type: string +description: The timestamp field name, which you use for time-based data views. \ No newline at end of file diff --git a/src/plugins/data_views/docs/openapi/components/schemas/title.yaml b/src/plugins/data_views/docs/openapi/components/schemas/title.yaml new file mode 100644 index 0000000000000..5cb76a853557b --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/schemas/title.yaml @@ -0,0 +1,2 @@ +type: string +description: Comma-separated list of data streams, indices, and aliases that you want to search. Supports wildcards (`*`). \ No newline at end of file diff --git a/src/plugins/data_views/docs/openapi/components/schemas/type.yaml b/src/plugins/data_views/docs/openapi/components/schemas/type.yaml new file mode 100644 index 0000000000000..f5e4261852e4d --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/schemas/type.yaml @@ -0,0 +1,2 @@ +type: string +description: When set to `rollup`, identifies the rollup data views. \ No newline at end of file diff --git a/src/plugins/data_views/docs/openapi/components/schemas/typemeta.yaml b/src/plugins/data_views/docs/openapi/components/schemas/typemeta.yaml new file mode 100644 index 0000000000000..995ee99f97edd --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/schemas/typemeta.yaml @@ -0,0 +1,2 @@ +type: object +description: When you use rollup indices, contains the field list for the rollup data view API endpoints. \ No newline at end of file diff --git a/src/plugins/data_views/docs/openapi/components/schemas/update_data_view_request_object.yaml b/src/plugins/data_views/docs/openapi/components/schemas/update_data_view_request_object.yaml new file mode 100644 index 0000000000000..777e5491e766b --- /dev/null +++ b/src/plugins/data_views/docs/openapi/components/schemas/update_data_view_request_object.yaml @@ -0,0 +1,35 @@ +title: Update data view request +type: object +required: + - data_view +properties: + data_view: + type: object + description: > + The data view properties you want to update. + Only the specified properties are updated in the data view. Unspecified fields stay as they are persisted. + properties: + allowNoIndex: + $ref: 'allownoindex.yaml' + fieldFormats: + $ref: 'fieldformats.yaml' + fields: + type: object + name: + type: string + runtimeFieldMap: + $ref: 'runtimefieldmap.yaml' + sourceFilters: + $ref: 'sourcefilters.yaml' + timeFieldName: + $ref: 'timefieldname.yaml' + title: + $ref: 'title.yaml' + type: + $ref: 'type.yaml' + typeMeta: + $ref: 'typemeta.yaml' + refresh_fields: + type: boolean + description: Reloads the data view fields after the data view is updated. + default: false diff --git a/src/plugins/data_views/docs/openapi/entrypoint.yaml b/src/plugins/data_views/docs/openapi/entrypoint.yaml new file mode 100644 index 0000000000000..a21c0b31df577 --- /dev/null +++ b/src/plugins/data_views/docs/openapi/entrypoint.yaml @@ -0,0 +1,59 @@ +openapi: 3.1.0 +info: + title: Data views + description: OpenAPI schema for data view endpoints + version: '0.1' + contact: + name: Kibana Core Team + license: + name: Elastic License 2.0 + url: https://www.elastic.co/licensing/elastic-license +tags: + - name: data views + description: Data view APIs enable you to manage data views, formerly known as Kibana index patterns. +servers: + - url: 'http://localhost:5601' + description: local +paths: +# Default space + '/api/data_views': + $ref: 'paths/api@data_views.yaml' + '/api/data_views/data_view': + $ref: 'paths/api@data_views@data_view.yaml' + '/api/data_views/data_view/{viewId}': + $ref: 'paths/api@data_views@data_view@{viewid}.yaml' +# '/api/data_views/data_view/{viewId}/fields': +# $ref: 'paths/api@data_views@data_view@{viewid}@fields.yaml' +# '/api/data_views/data_view/{viewId}/runtime_field': +# $ref: 'paths/api@data_views@data_view@{viewid}@runtime_field.yaml' + '/api/data_views/data_view/{viewId}/runtime_field/{fieldName}': + $ref: 'paths/api@data_views@data_view@{viewid}@runtime_field@{fieldname}.yaml' + '/api/data_views/default': + $ref: 'paths/api@data_views@default.yaml' +# Non-default space +# '/s/{spaceId}/api/data_views': +# $ref: 'paths/s@{spaceid}@api@data_views.yaml' +# '/s/{spaceId}/api/data_views/data_view': +# $ref: 'paths/s@{spaceid}@api@data_views@data_view.yaml' +# '/s/{spaceId}/api/data_views/data_view/{viewId}': +# $ref: 'paths/s@{spaceid}@api@data_views@data_view@{viewid}.yaml' +# '/s/{spaceId}/api/data_views/default': +# $ref: 'paths/s@{spaceid}@api@data_views@default.yaml' +# '/s/{spaceId}/api/data_views/data_view/{viewId}/fields': +# $ref: 'paths/s@{spaceid}@api@data_views@data_view@{viewid}@fields.yaml' +# '/s/{spaceId}/api/data_views/data_view/{viewId}/runtime_field': +# $ref: 'paths/s@{spaceid}@api@data_views@data_view@{viewid}@runtime_field.yaml' +# '/s/{spaceId}/api/data_views/data_view/{viewId}/runtime_field/{fieldName}': +# $ref: 'paths/s@{spaceid}@api@data_views@data_view@{viewid}@runtime_field@{fieldname}.yaml' +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + apiKeyAuth: + type: apiKey + in: header + name: ApiKey +security: + - basicAuth: [] + - apiKeyAuth: [] diff --git a/src/plugins/data_views/docs/openapi/paths/api@data_views.yaml b/src/plugins/data_views/docs/openapi/paths/api@data_views.yaml new file mode 100644 index 0000000000000..9a3278f5ecbac --- /dev/null +++ b/src/plugins/data_views/docs/openapi/paths/api@data_views.yaml @@ -0,0 +1,37 @@ +get: + summary: Retrieves a list of all data views. + operationId: getAllDataViews + description: > + This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. + tags: + - data views + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: object + properties: + data_view: + type: array + items: + type: object + properties: + id: + type: string + name: + type: string + namespaces: + type: array + items: + type: string + title: + type: string + typeMeta: + type: object + examples: + getAllDataViewsResponse: + $ref: '../components/examples/get_data_views_response.yaml' +servers: + - url: https://localhost:5601 diff --git a/src/plugins/data_views/docs/openapi/paths/api@data_views@data_view.yaml b/src/plugins/data_views/docs/openapi/paths/api@data_views@data_view.yaml new file mode 100644 index 0000000000000..dd8f3cdfac2bf --- /dev/null +++ b/src/plugins/data_views/docs/openapi/paths/api@data_views@data_view.yaml @@ -0,0 +1,36 @@ +post: + summary: Creates a data view. + operationId: createDataView + description: > + This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. + tags: + - data views + parameters: + - $ref: '../components/headers/kbn_xsrf.yaml' + requestBody: + required: true + content: + application/json: + schema: + $ref: '../components/schemas/create_data_view_request_object.yaml' + examples: + createDataViewRequest: + $ref: '../components/examples/create_data_view_request.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '../components/schemas/data_view_response_object.yaml' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '../components/schemas/400_response.yaml' + servers: + - url: https://localhost:5601 + +servers: + - url: https://localhost:5601 diff --git a/src/plugins/data_views/docs/openapi/paths/api@data_views@data_view@{viewid}.yaml b/src/plugins/data_views/docs/openapi/paths/api@data_views@data_view@{viewid}.yaml new file mode 100644 index 0000000000000..41f8875243057 --- /dev/null +++ b/src/plugins/data_views/docs/openapi/paths/api@data_views@data_view@{viewid}.yaml @@ -0,0 +1,85 @@ +get: + summary: Retrieves a single data view by identifier. + operationId: getDataView + description: > + This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. + tags: + - data views + parameters: + - $ref: '../components/parameters/view_id.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '../components/schemas/data_view_response_object.yaml' + examples: + getDataViewResponse: + $ref: '../components/examples/get_data_view_response.yaml' + '404': + description: Object is not found. + content: + application/json: + schema: + $ref: '../components/schemas/404_response.yaml' + +delete: + summary: Deletes a data view. + operationId: deleteDataView + description: > + WARNING: When you delete a data view, it cannot be recovered. + This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. + tags: + - data views + parameters: + - $ref: '../components/parameters/view_id.yaml' + responses: + '204': + description: Indicates a successful call. + '404': + description: Object is not found. + content: + application/json: + schema: + $ref: '../components/schemas/404_response.yaml' + servers: + - url: https://localhost:5601 + +post: + summary: Updates a data view. + operationId: updateDataView + description: > + This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. + tags: + - data views + parameters: + - $ref: '../components/headers/kbn_xsrf.yaml' + - $ref: '../components/parameters/view_id.yaml' + requestBody: + required: true + content: + application/json: + schema: + $ref: '../components/schemas/update_data_view_request_object.yaml' + examples: + updateDataViewRequest: + $ref: '../components/examples/update_data_view_request.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '../components/schemas/data_view_response_object.yaml' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '../components/schemas/400_response.yaml' + servers: + - url: https://localhost:5601 + +servers: + - url: https://localhost:5601 diff --git a/src/plugins/data_views/docs/openapi/paths/api@data_views@data_view@{viewid}@runtime_field@{fieldname}.yaml b/src/plugins/data_views/docs/openapi/paths/api@data_views@data_view@{viewid}@runtime_field@{fieldname}.yaml new file mode 100644 index 0000000000000..15c5659f09d4a --- /dev/null +++ b/src/plugins/data_views/docs/openapi/paths/api@data_views@data_view@{viewid}@runtime_field@{fieldname}.yaml @@ -0,0 +1,35 @@ +get: + summary: Retrieves a runtime field. + operationId: getRuntimeField + description: > + This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. + tags: + - data views + parameters: + - $ref: '../components/parameters/field_name.yaml' + - $ref: '../components/parameters/view_id.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: object + properties: + data_view: + type: object + fields: + type: array + items: + type: object + examples: + getRuntimeFieldResponse: + $ref: '../components/examples/get_runtime_field_response.yaml' + '404': + description: Object is not found. + content: + application/json: + schema: + $ref: '../components/schemas/404_response.yaml' +servers: + - url: https://localhost:5601 diff --git a/src/plugins/data_views/docs/openapi/paths/api@data_views@default.yaml b/src/plugins/data_views/docs/openapi/paths/api@data_views@default.yaml new file mode 100644 index 0000000000000..cf1b3151380a9 --- /dev/null +++ b/src/plugins/data_views/docs/openapi/paths/api@data_views@default.yaml @@ -0,0 +1,67 @@ +get: + summary: Retrieves the default data view identifier. + operationId: getDefaultDataView + description: > + This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. + tags: + - data views + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: object + properties: + data_view_id: + type: string + examples: + getDefaultDataViewResponse: + $ref: '../components/examples/get_default_data_view_response.yaml' + +post: + summary: Sets the default data view identifier. + operationId: setDefaultDatailView + description: > + This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. + tags: + - data views + parameters: + - $ref: '../components/headers/kbn_xsrf.yaml' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - data_view_id + properties: + data_view_id: + type: ['string', 'null'] + description: > + The data view identifier. + NOTE: The API does not validate whether it is a valid identifier. + Use `null` to unset the default data view. + force: + type: boolean + description: Update an existing default data view identifier. + default: false + examples: + setDefaultDataViewRequest: + $ref: '../components/examples/set_default_data_view_request.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: object + properties: + acknowledged: + type: boolean + servers: + - url: https://localhost:5601 + +servers: + - url: https://localhost:5601 diff --git a/src/plugins/discover/common/constants.ts b/src/plugins/discover/common/constants.ts index 8e61baa8ba6fc..87a9378fa963c 100644 --- a/src/plugins/discover/common/constants.ts +++ b/src/plugins/discover/common/constants.ts @@ -12,3 +12,5 @@ export enum VIEW_MODE { DOCUMENT_LEVEL = 'documents', AGGREGATED_LEVEL = 'aggregated', } + +export const DISABLE_SHARD_FAILURE_WARNING = true; diff --git a/src/plugins/discover/public/application/context/components/action_bar/action_bar.tsx b/src/plugins/discover/public/application/context/components/action_bar/action_bar.tsx index 0168e2f82e508..5cbb72f0602ee 100644 --- a/src/plugins/discover/public/application/context/components/action_bar/action_bar.tsx +++ b/src/plugins/discover/public/application/context/components/action_bar/action_bar.tsx @@ -154,6 +154,7 @@ export function ActionBar({ + {!isSuccessor && showWarning && } {!isSuccessor && showWarning && } {!isSuccessor && } diff --git a/src/plugins/discover/public/application/context/components/action_bar/action_bar_warning.tsx b/src/plugins/discover/public/application/context/components/action_bar/action_bar_warning.tsx index 65ad945429ced..d833993aadfd7 100644 --- a/src/plugins/discover/public/application/context/components/action_bar/action_bar_warning.tsx +++ b/src/plugins/discover/public/application/context/components/action_bar/action_bar_warning.tsx @@ -15,9 +15,9 @@ export function ActionBarWarning({ docCount, type }: { docCount: number; type: S if (type === SurrDocType.PREDECESSORS) { return ( [fetchedState.predecessors, fetchedState.anchor, fetchedState.successors] ); + const interceptedWarnings = useMemo( + () => + removeInterceptedWarningDuplicates([ + ...(fetchedState.predecessorsInterceptedWarnings || []), + ...(fetchedState.anchorInterceptedWarnings || []), + ...(fetchedState.successorsInterceptedWarnings || []), + ]), + [ + fetchedState.predecessorsInterceptedWarnings, + fetchedState.anchorInterceptedWarnings, + fetchedState.successorsInterceptedWarnings, + ] + ); + const addFilter = useCallback( async (field: DataViewField | string, values: unknown, operation: string) => { const newFilters = generateFilters(filterManager, field, values, operation, dataView); @@ -251,6 +266,7 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) => anchorStatus={fetchedState.anchorStatus.value} predecessorsStatus={fetchedState.predecessorsStatus.value} successorsStatus={fetchedState.successorsStatus.value} + interceptedWarnings={interceptedWarnings} /> diff --git a/src/plugins/discover/public/application/context/context_app_content.tsx b/src/plugins/discover/public/application/context/context_app_content.tsx index 940c426817a96..fa9ed3b96c9ff 100644 --- a/src/plugins/discover/public/application/context/context_app_content.tsx +++ b/src/plugins/discover/public/application/context/context_app_content.tsx @@ -8,12 +8,16 @@ import React, { useState, Fragment, useMemo, useCallback } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiHorizontalRule, EuiText } from '@elastic/eui'; +import { EuiHorizontalRule, EuiSpacer, EuiText } from '@elastic/eui'; import type { DataView } from '@kbn/data-views-plugin/public'; import { SortDirection } from '@kbn/data-plugin/public'; import type { SortOrder } from '@kbn/saved-search-plugin/public'; import { CellActionsProvider } from '@kbn/cell-actions'; import type { DataTableRecord } from '@kbn/discover-utils/types'; +import { + SearchResponseWarnings, + type SearchResponseInterceptedWarning, +} from '@kbn/search-response-warnings'; import { CONTEXT_STEP_SETTING, DOC_HIDE_TIME_COLUMN_SETTING } from '@kbn/discover-utils'; import { LoadingStatus } from './services/context_query_state'; import { ActionBar } from './components/action_bar/action_bar'; @@ -41,6 +45,7 @@ export interface ContextAppContentProps { anchorStatus: LoadingStatus; predecessorsStatus: LoadingStatus; successorsStatus: LoadingStatus; + interceptedWarnings: SearchResponseInterceptedWarning[] | undefined; useNewFieldsApi: boolean; isLegacy: boolean; setAppState: (newState: Partial) => void; @@ -71,6 +76,7 @@ export function ContextAppContent({ anchorStatus, predecessorsStatus, successorsStatus, + interceptedWarnings, useNewFieldsApi, isLegacy, setAppState, @@ -118,6 +124,16 @@ export function ContextAppContent({ return ( + {!!interceptedWarnings?.length && ( + <> + + + + )} ({ + originalWarning, +})); + const mockFilterManager = createFilterManagerMock(); +let mockOverrideInterceptedWarnings = false; + jest.mock('../services/context', () => { const originalModule = jest.requireActual('../services/context'); return { @@ -35,7 +42,12 @@ jest.mock('../services/context', () => { if (!dataView || !dataView.id) { throw new Error(); } - return type === 'predecessors' ? mockPredecessorHits : mockSuccessorHits; + return { + rows: type === 'predecessors' ? mockPredecessorHits : mockSuccessorHits, + interceptedWarnings: mockOverrideInterceptedWarnings + ? [mockInterceptedWarnings[type === 'predecessors' ? 0 : 1]] + : undefined, + }; }, }; }); @@ -45,7 +57,12 @@ jest.mock('../services/anchor', () => ({ if (!dataView.id || !anchorId) { throw new Error(); } - return mockAnchorHit; + return { + anchorRow: mockAnchorHit, + interceptedWarnings: mockOverrideInterceptedWarnings + ? [mockInterceptedWarnings[2]] + : undefined, + }; }, })); @@ -118,6 +135,9 @@ describe('test useContextAppFetch', () => { expect(result.current.fetchedState.anchor).toEqual({ ...mockAnchorHit, isAnchor: true }); expect(result.current.fetchedState.predecessors).toEqual(mockPredecessorHits); expect(result.current.fetchedState.successors).toEqual(mockSuccessorHits); + expect(result.current.fetchedState.predecessorsInterceptedWarnings).toBeUndefined(); + expect(result.current.fetchedState.successorsInterceptedWarnings).toBeUndefined(); + expect(result.current.fetchedState.anchorInterceptedWarnings).toBeUndefined(); }); it('should set anchorStatus to failed when tieBreakingField array is empty', async () => { @@ -187,4 +207,34 @@ describe('test useContextAppFetch', () => { expect(result.current.fetchedState.predecessors).toEqual([]); expect(result.current.fetchedState.successors).toEqual([]); }); + + it('should handle warnings', async () => { + mockOverrideInterceptedWarnings = true; + + const { result } = initDefaults(['_doc']); + + expect(result.current.fetchedState.anchorStatus.value).toBe(LoadingStatus.UNINITIALIZED); + expect(result.current.fetchedState.predecessorsStatus.value).toBe(LoadingStatus.UNINITIALIZED); + expect(result.current.fetchedState.successorsStatus.value).toBe(LoadingStatus.UNINITIALIZED); + + await act(async () => { + await result.current.fetchAllRows(); + }); + + expect(result.current.fetchedState.anchorStatus.value).toBe(LoadingStatus.LOADED); + expect(result.current.fetchedState.predecessorsStatus.value).toBe(LoadingStatus.LOADED); + expect(result.current.fetchedState.successorsStatus.value).toBe(LoadingStatus.LOADED); + expect(result.current.fetchedState.anchor).toEqual({ ...mockAnchorHit, isAnchor: true }); + expect(result.current.fetchedState.predecessors).toEqual(mockPredecessorHits); + expect(result.current.fetchedState.successors).toEqual(mockSuccessorHits); + expect(result.current.fetchedState.predecessorsInterceptedWarnings).toEqual([ + mockInterceptedWarnings[0], + ]); + expect(result.current.fetchedState.successorsInterceptedWarnings).toEqual([ + mockInterceptedWarnings[1], + ]); + expect(result.current.fetchedState.anchorInterceptedWarnings).toEqual([ + mockInterceptedWarnings[2], + ]); + }); }); diff --git a/src/plugins/discover/public/application/context/hooks/use_context_app_fetch.tsx b/src/plugins/discover/public/application/context/hooks/use_context_app_fetch.tsx index 0f7da6d678cfd..a2069f22c97c0 100644 --- a/src/plugins/discover/public/application/context/hooks/use_context_app_fetch.tsx +++ b/src/plugins/discover/public/application/context/hooks/use_context_app_fetch.tsx @@ -41,13 +41,8 @@ export function useContextAppFetch({ appState, useNewFieldsApi, }: ContextAppFetchProps) { - const { - uiSettings: config, - data, - toastNotifications, - filterManager, - core, - } = useDiscoverServices(); + const services = useDiscoverServices(); + const { uiSettings: config, data, toastNotifications, filterManager, core } = services; const { theme$ } = core.theme; const searchSource = useMemo(() => { @@ -95,9 +90,20 @@ export function useContextAppFetch({ { [dataView.timeFieldName!]: SortDirection.desc }, { [tieBreakerField]: SortDirection.desc }, ]; - const anchor = await fetchAnchor(anchorId, dataView, searchSource, sort, useNewFieldsApi); - setState({ anchor, anchorStatus: { value: LoadingStatus.LOADED } }); - return anchor; + const result = await fetchAnchor( + anchorId, + dataView, + searchSource, + sort, + useNewFieldsApi, + services + ); + setState({ + anchor: result.anchorRow, + anchorInterceptedWarnings: result.interceptedWarnings, + anchorStatus: { value: LoadingStatus.LOADED }, + }); + return result.anchorRow; } catch (error) { setState(createError('anchorStatus', FailureReason.UNKNOWN, error)); toastNotifications.addDanger({ @@ -106,6 +112,7 @@ export function useContextAppFetch({ }); } }, [ + services, tieBreakerField, setState, toastNotifications, @@ -124,13 +131,14 @@ export function useContextAppFetch({ type === SurrDocType.PREDECESSORS ? appState.predecessorCount : appState.successorCount; const anchor = fetchedAnchor || fetchedState.anchor; const statusKey = `${type}Status`; + const warningsKey = `${type}InterceptedWarnings`; const errorTitle = i18n.translate('discover.context.unableToLoadDocumentDescription', { defaultMessage: 'Unable to load documents', }); try { setState({ [statusKey]: { value: LoadingStatus.LOADING } }); - const rows = anchor.id + const result = anchor.id ? await fetchSurroundingDocs( type, dataView, @@ -140,10 +148,15 @@ export function useContextAppFetch({ count, filters, data, - useNewFieldsApi + useNewFieldsApi, + services ) - : []; - setState({ [type]: rows, [statusKey]: { value: LoadingStatus.LOADED } }); + : { rows: [], interceptedWarnings: undefined }; + setState({ + [type]: result.rows, + [warningsKey]: result.interceptedWarnings, + [statusKey]: { value: LoadingStatus.LOADED }, + }); } catch (error) { setState(createError(statusKey, FailureReason.UNKNOWN, error)); toastNotifications.addDanger({ @@ -155,6 +168,7 @@ export function useContextAppFetch({ } }, [ + services, filterManager, appState, fetchedState.anchor, diff --git a/src/plugins/discover/public/application/context/services/anchor.test.ts b/src/plugins/discover/public/application/context/services/anchor.test.ts index a20b2d454bcdd..415b468f38afd 100644 --- a/src/plugins/discover/public/application/context/services/anchor.test.ts +++ b/src/plugins/discover/public/application/context/services/anchor.test.ts @@ -10,7 +10,9 @@ import { SortDirection } from '@kbn/data-plugin/public'; import { createSearchSourceStub } from './_stubs'; import { fetchAnchor, updateSearchSource } from './anchor'; import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; +import { searchResponseTimeoutWarningMock } from '@kbn/search-response-warnings/src/__mocks__/search_response_warnings'; import { savedSearchMock } from '../../../__mocks__/saved_search'; +import { discoverServiceMock } from '../../../__mocks__/services'; describe('context app', function () { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -27,19 +29,27 @@ describe('context app', function () { }); it('should use the `fetch$` method of the SearchSource', function () { - return fetchAnchor('id', dataView, searchSourceStub, [ - { '@timestamp': SortDirection.desc }, - { _doc: SortDirection.desc }, - ]).then(() => { + return fetchAnchor( + 'id', + dataView, + searchSourceStub, + [{ '@timestamp': SortDirection.desc }, { _doc: SortDirection.desc }], + false, + discoverServiceMock + ).then(() => { expect(searchSourceStub.fetch$.calledOnce).toBe(true); }); }); it('should configure the SearchSource to not inherit from the implicit root', function () { - return fetchAnchor('id', dataView, searchSourceStub, [ - { '@timestamp': SortDirection.desc }, - { _doc: SortDirection.desc }, - ]).then(() => { + return fetchAnchor( + 'id', + dataView, + searchSourceStub, + [{ '@timestamp': SortDirection.desc }, { _doc: SortDirection.desc }], + false, + discoverServiceMock + ).then(() => { const setParentSpy = searchSourceStub.setParent; expect(setParentSpy.calledOnce).toBe(true); expect(setParentSpy.firstCall.args[0]).toBe(undefined); @@ -47,20 +57,28 @@ describe('context app', function () { }); it('should set the SearchSource data view', function () { - return fetchAnchor('id', dataView, searchSourceStub, [ - { '@timestamp': SortDirection.desc }, - { _doc: SortDirection.desc }, - ]).then(() => { + return fetchAnchor( + 'id', + dataView, + searchSourceStub, + [{ '@timestamp': SortDirection.desc }, { _doc: SortDirection.desc }], + false, + discoverServiceMock + ).then(() => { const setFieldSpy = searchSourceStub.setField; expect(setFieldSpy.firstCall.args[1].id).toEqual('DATA_VIEW_ID'); }); }); it('should set the SearchSource version flag to true', function () { - return fetchAnchor('id', dataView, searchSourceStub, [ - { '@timestamp': SortDirection.desc }, - { _doc: SortDirection.desc }, - ]).then(() => { + return fetchAnchor( + 'id', + dataView, + searchSourceStub, + [{ '@timestamp': SortDirection.desc }, { _doc: SortDirection.desc }], + false, + discoverServiceMock + ).then(() => { const setVersionSpy = searchSourceStub.setField.withArgs('version'); expect(setVersionSpy.calledOnce).toBe(true); expect(setVersionSpy.firstCall.args[1]).toEqual(true); @@ -68,10 +86,14 @@ describe('context app', function () { }); it('should set the SearchSource size to 1', function () { - return fetchAnchor('id', dataView, searchSourceStub, [ - { '@timestamp': SortDirection.desc }, - { _doc: SortDirection.desc }, - ]).then(() => { + return fetchAnchor( + 'id', + dataView, + searchSourceStub, + [{ '@timestamp': SortDirection.desc }, { _doc: SortDirection.desc }], + false, + discoverServiceMock + ).then(() => { const setSizeSpy = searchSourceStub.setField.withArgs('size'); expect(setSizeSpy.calledOnce).toBe(true); expect(setSizeSpy.firstCall.args[1]).toEqual(1); @@ -79,10 +101,14 @@ describe('context app', function () { }); it('should set the SearchSource query to an ids query', function () { - return fetchAnchor('id', dataView, searchSourceStub, [ - { '@timestamp': SortDirection.desc }, - { _doc: SortDirection.desc }, - ]).then(() => { + return fetchAnchor( + 'id', + dataView, + searchSourceStub, + [{ '@timestamp': SortDirection.desc }, { _doc: SortDirection.desc }], + false, + discoverServiceMock + ).then(() => { const setQuerySpy = searchSourceStub.setField.withArgs('query'); expect(setQuerySpy.calledOnce).toBe(true); expect(setQuerySpy.firstCall.args[1]).toEqual({ @@ -101,10 +127,14 @@ describe('context app', function () { }); it('should set the SearchSource sort order', function () { - return fetchAnchor('id', dataView, searchSourceStub, [ - { '@timestamp': SortDirection.desc }, - { _doc: SortDirection.desc }, - ]).then(() => { + return fetchAnchor( + 'id', + dataView, + searchSourceStub, + [{ '@timestamp': SortDirection.desc }, { _doc: SortDirection.desc }], + false, + discoverServiceMock + ).then(() => { const setSortSpy = searchSourceStub.setField.withArgs('sort'); expect(setSortSpy.calledOnce).toBe(true); expect(setSortSpy.firstCall.args[1]).toEqual([ @@ -143,10 +173,14 @@ describe('context app', function () { it('should reject with an error when no hits were found', function () { searchSourceStub = createSearchSourceStub([]); - return fetchAnchor('id', dataView, searchSourceStub, [ - { '@timestamp': SortDirection.desc }, - { _doc: SortDirection.desc }, - ]).then( + return fetchAnchor( + 'id', + dataView, + searchSourceStub, + [{ '@timestamp': SortDirection.desc }, { _doc: SortDirection.desc }], + false, + discoverServiceMock + ).then( () => { fail('expected the promise to be rejected'); }, @@ -162,12 +196,53 @@ describe('context app', function () { { _id: '3', _index: 't' }, ]); - return fetchAnchor('id', dataView, searchSourceStub, [ - { '@timestamp': SortDirection.desc }, - { _doc: SortDirection.desc }, - ]).then((anchorDocument) => { - expect(anchorDocument).toHaveProperty('raw._id', '1'); - expect(anchorDocument).toHaveProperty('isAnchor', true); + return fetchAnchor( + 'id', + dataView, + searchSourceStub, + [{ '@timestamp': SortDirection.desc }, { _doc: SortDirection.desc }], + false, + discoverServiceMock + ).then(({ anchorRow, interceptedWarnings }) => { + expect(anchorRow).toHaveProperty('raw._id', '1'); + expect(anchorRow).toHaveProperty('isAnchor', true); + expect(interceptedWarnings).toBeUndefined(); + }); + }); + + it('should intercept shard failures', function () { + searchSourceStub = createSearchSourceStub([ + { _id: '1', _index: 't' }, + { _id: '3', _index: 't' }, + ]); + + const mockWarnings = [ + { + originalWarning: searchResponseTimeoutWarningMock, + }, + ]; + + const services = discoverServiceMock; + services.data.search.showWarnings = jest.fn((adapter, callback) => { + // @ts-expect-error for empty meta + callback?.(mockWarnings[0].originalWarning, {}); + + // plus duplicates + // @ts-expect-error for empty meta + callback?.(mockWarnings[0].originalWarning, {}); + }); + + return fetchAnchor( + 'id', + dataView, + searchSourceStub, + [{ '@timestamp': SortDirection.desc }, { _doc: SortDirection.desc }], + false, + services + ).then(({ anchorRow, interceptedWarnings }) => { + expect(anchorRow).toHaveProperty('raw._id', '1'); + expect(anchorRow).toHaveProperty('isAnchor', true); + expect(interceptedWarnings).toEqual(mockWarnings); }); }); }); @@ -185,7 +260,8 @@ describe('context app', function () { dataView, searchSourceStub, [{ '@timestamp': SortDirection.desc }, { _doc: SortDirection.desc }], - true + true, + discoverServiceMock ).then(() => { const setFieldsSpy = searchSourceStub.setField.withArgs('fields'); const removeFieldsSpy = searchSourceStub.removeField.withArgs('fieldsFromSource'); diff --git a/src/plugins/discover/public/application/context/services/anchor.ts b/src/plugins/discover/public/application/context/services/anchor.ts index dce35fea93282..daf99030201a4 100644 --- a/src/plugins/discover/public/application/context/services/anchor.ts +++ b/src/plugins/discover/public/application/context/services/anchor.ts @@ -8,19 +8,40 @@ import { lastValueFrom } from 'rxjs'; import { i18n } from '@kbn/i18n'; import { ISearchSource, EsQuerySortValue } from '@kbn/data-plugin/public'; -import { DataView } from '@kbn/data-views-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { buildDataTableRecord } from '@kbn/discover-utils'; import type { DataTableRecord, EsHitRecord } from '@kbn/discover-utils/types'; +import { + getSearchResponseInterceptedWarnings, + type SearchResponseInterceptedWarning, +} from '@kbn/search-response-warnings'; +import type { DiscoverServices } from '../../../build_services'; +import { DISABLE_SHARD_FAILURE_WARNING } from '../../../../common/constants'; export async function fetchAnchor( anchorId: string, dataView: DataView, searchSource: ISearchSource, sort: EsQuerySortValue[], - useNewFieldsApi: boolean = false -): Promise { + useNewFieldsApi: boolean = false, + services: DiscoverServices +): Promise<{ + anchorRow: DataTableRecord; + interceptedWarnings: SearchResponseInterceptedWarning[] | undefined; +}> { updateSearchSource(searchSource, anchorId, sort, useNewFieldsApi, dataView); - const { rawResponse } = await lastValueFrom(searchSource.fetch$()); + + const adapter = new RequestAdapter(); + const { rawResponse } = await lastValueFrom( + searchSource.fetch$({ + disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING, + inspector: { + adapter, + title: 'anchor', + }, + }) + ); const doc = rawResponse.hits?.hits?.[0] as EsHitRecord; if (!doc) { @@ -30,7 +51,16 @@ export async function fetchAnchor( }) ); } - return buildDataTableRecord(doc, dataView, true); + return { + anchorRow: buildDataTableRecord(doc, dataView, true), + interceptedWarnings: getSearchResponseInterceptedWarnings({ + services, + adapter, + options: { + disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING, + }, + }), + }; } export function updateSearchSource( diff --git a/src/plugins/discover/public/application/context/services/context.predecessors.test.ts b/src/plugins/discover/public/application/context/services/context.predecessors.test.ts index cf06344eace72..451c38e87399a 100644 --- a/src/plugins/discover/public/application/context/services/context.predecessors.test.ts +++ b/src/plugins/discover/public/application/context/services/context.predecessors.test.ts @@ -14,8 +14,9 @@ import { Query } from '@kbn/es-query'; import { createContextSearchSourceStub } from './_stubs'; import { fetchSurroundingDocs, SurrDocType } from './context'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import type { DataTableRecord, EsHitRecord } from '@kbn/discover-utils/types'; +import type { EsHitRecord } from '@kbn/discover-utils/types'; import { buildDataTableRecord, buildDataTableRecordList } from '@kbn/discover-utils'; +import { discoverServiceMock } from '../../../__mocks__/services'; const MS_PER_DAY = 24 * 60 * 60 * 1000; const ANCHOR_TIMESTAMP = new Date(MS_PER_DAY).toJSON(); @@ -37,7 +38,7 @@ describe('context predecessors', function () { tieBreakerField: string, tieBreakerValue: number, size: number - ) => Promise; + ) => ReturnType; // eslint-disable-next-line @typescript-eslint/no-explicit-any let mockSearchSource: any; @@ -82,7 +83,9 @@ describe('context predecessors', function () { SortDirection.desc, size, [], - dataPluginMock + dataPluginMock, + false, + discoverServiceMock ); }; }); @@ -97,9 +100,9 @@ describe('context predecessors', function () { ]; return fetchPredecessors(ANCHOR_TIMESTAMP_3000, MS_PER_DAY * 3000, '_doc', 0, 3).then( - (hits) => { + ({ rows }) => { expect(mockSearchSource.fetch$.calledOnce).toBe(true); - expect(hits).toEqual( + expect(rows).toEqual( buildDataTableRecordList(mockSearchSource._stubHits.slice(0, 3), dataView) ); } @@ -116,7 +119,7 @@ describe('context predecessors', function () { ]; return fetchPredecessors(ANCHOR_TIMESTAMP_3000, MS_PER_DAY * 3000, '_doc', 0, 6).then( - (hits) => { + ({ rows }) => { const intervals: Timestamp[] = mockSearchSource.setField.args .filter(([property]: string) => property === 'query') .map(([, { query }]: [string, { query: Query }]) => @@ -131,7 +134,7 @@ describe('context predecessors', function () { // should have ended with a half-open interval expect(Object.keys(last(intervals) ?? {})).toEqual(['format', 'gte']); expect(intervals.length).toBeGreaterThan(1); - expect(hits).toEqual( + expect(rows).toEqual( buildDataTableRecordList(mockSearchSource._stubHits.slice(0, 3), dataView) ); } @@ -147,7 +150,7 @@ describe('context predecessors', function () { ]; return fetchPredecessors(ANCHOR_TIMESTAMP_1000, MS_PER_DAY * 1000, '_doc', 0, 3).then( - (hits) => { + ({ rows }) => { const intervals: Timestamp[] = mockSearchSource.setField.args .filter(([property]: string) => property === 'query') .map(([, { query }]: [string, { query: Query }]) => { @@ -167,7 +170,7 @@ describe('context predecessors', function () { expect(moment(last(intervals)?.lte).valueOf()).toBeLessThan(MS_PER_DAY * 1700); expect(intervals.length).toBeGreaterThan(1); - expect(hits).toEqual( + expect(rows).toEqual( buildDataTableRecordList(mockSearchSource._stubHits.slice(-3), dataView) ); } @@ -175,9 +178,11 @@ describe('context predecessors', function () { }); it('should return an empty array when no hits were found', function () { - return fetchPredecessors(ANCHOR_TIMESTAMP_3, MS_PER_DAY * 3, '_doc', 0, 3).then((hits) => { - expect(hits).toEqual([]); - }); + return fetchPredecessors(ANCHOR_TIMESTAMP_3, MS_PER_DAY * 3, '_doc', 0, 3).then( + ({ rows }) => { + expect(rows).toEqual([]); + } + ); }); it('should configure the SearchSource to not inherit from the implicit root', function () { @@ -233,7 +238,8 @@ describe('context predecessors', function () { size, [], dataPluginMock, - true + true, + discoverServiceMock ); }; }); @@ -248,13 +254,13 @@ describe('context predecessors', function () { ]; return fetchPredecessors(ANCHOR_TIMESTAMP_3000, MS_PER_DAY * 3000, '_doc', 0, 3).then( - (hits) => { + ({ rows }) => { const setFieldsSpy = mockSearchSource.setField.withArgs('fields'); const removeFieldsSpy = mockSearchSource.removeField.withArgs('fieldsFromSource'); expect(mockSearchSource.fetch$.calledOnce).toBe(true); expect(removeFieldsSpy.calledOnce).toBe(true); expect(setFieldsSpy.calledOnce).toBe(true); - expect(hits).toEqual( + expect(rows).toEqual( buildDataTableRecordList(mockSearchSource._stubHits.slice(0, 3), dataView) ); } diff --git a/src/plugins/discover/public/application/context/services/context.successors.test.ts b/src/plugins/discover/public/application/context/services/context.successors.test.ts index 049d9efa2abf7..9c2a0120c2a72 100644 --- a/src/plugins/discover/public/application/context/services/context.successors.test.ts +++ b/src/plugins/discover/public/application/context/services/context.successors.test.ts @@ -14,8 +14,8 @@ import { createContextSearchSourceStub } from './_stubs'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { Query } from '@kbn/es-query'; import { fetchSurroundingDocs, SurrDocType } from './context'; -import type { DataTableRecord } from '@kbn/discover-utils/types'; import { buildDataTableRecord, buildDataTableRecordList } from '@kbn/discover-utils'; +import { discoverServiceMock } from '../../../__mocks__/services'; const MS_PER_DAY = 24 * 60 * 60 * 1000; const ANCHOR_TIMESTAMP = new Date(MS_PER_DAY).toJSON(); @@ -35,7 +35,7 @@ describe('context successors', function () { tieBreakerField: string, tieBreakerValue: number, size: number - ) => Promise; + ) => ReturnType; let dataPluginMock: DataPublicPluginStart; // eslint-disable-next-line @typescript-eslint/no-explicit-any let mockSearchSource: any; @@ -83,7 +83,9 @@ describe('context successors', function () { SortDirection.desc, size, [], - dataPluginMock + dataPluginMock, + false, + discoverServiceMock ); }; }); @@ -98,9 +100,9 @@ describe('context successors', function () { ]; return fetchSuccessors(ANCHOR_TIMESTAMP_3000, MS_PER_DAY * 3000, '_doc', 0, 3).then( - (hits) => { + ({ rows }) => { expect(mockSearchSource.fetch$.calledOnce).toBe(true); - expect(hits).toEqual( + expect(rows).toEqual( buildDataTableRecordList(mockSearchSource._stubHits.slice(-3), dataView) ); } @@ -117,7 +119,7 @@ describe('context successors', function () { ]; return fetchSuccessors(ANCHOR_TIMESTAMP_3000, MS_PER_DAY * 3000, '_doc', 0, 6).then( - (hits) => { + ({ rows }) => { const intervals: Timestamp[] = mockSearchSource.setField.args .filter(([property]: [string]) => property === 'query') .map(([, { query }]: [string, { query: Query }]) => @@ -132,7 +134,7 @@ describe('context successors', function () { // should have ended with a half-open interval expect(Object.keys(last(intervals) ?? {})).toEqual(['format', 'lte']); expect(intervals.length).toBeGreaterThan(1); - expect(hits).toEqual( + expect(rows).toEqual( buildDataTableRecordList(mockSearchSource._stubHits.slice(-3), dataView) ); } @@ -150,7 +152,7 @@ describe('context successors', function () { ]; return fetchSuccessors(ANCHOR_TIMESTAMP_3000, MS_PER_DAY * 3000, '_doc', 0, 4).then( - (hits) => { + ({ rows }) => { const intervals: Timestamp[] = mockSearchSource.setField.args .filter(([property]: [string]) => property === 'query') .map(([, { query }]: [string, { query: Query }]) => @@ -162,7 +164,7 @@ describe('context successors', function () { // should have stopped before reaching MS_PER_DAY * 2200 expect(moment(last(intervals)?.gte).valueOf()).toBeGreaterThan(MS_PER_DAY * 2200); expect(intervals.length).toBeGreaterThan(1); - expect(hits).toEqual( + expect(rows).toEqual( buildDataTableRecordList(mockSearchSource._stubHits.slice(0, 4), dataView) ); } @@ -170,8 +172,8 @@ describe('context successors', function () { }); it('should return an empty array when no hits were found', function () { - return fetchSuccessors(ANCHOR_TIMESTAMP_3, MS_PER_DAY * 3, '_doc', 0, 3).then((hits) => { - expect(hits).toEqual([]); + return fetchSuccessors(ANCHOR_TIMESTAMP_3, MS_PER_DAY * 3, '_doc', 0, 3).then(({ rows }) => { + expect(rows).toEqual([]); }); }); @@ -230,7 +232,8 @@ describe('context successors', function () { size, [], dataPluginMock, - true + true, + discoverServiceMock ); }; }); @@ -245,15 +248,104 @@ describe('context successors', function () { ]; return fetchSuccessors(ANCHOR_TIMESTAMP_3000, MS_PER_DAY * 3000, '_doc', 0, 3).then( - (hits) => { + ({ rows, interceptedWarnings }) => { expect(mockSearchSource.fetch$.calledOnce).toBe(true); - expect(hits).toEqual( + expect(rows).toEqual( buildDataTableRecordList(mockSearchSource._stubHits.slice(-3), dataView) ); const setFieldsSpy = mockSearchSource.setField.withArgs('fields'); const removeFieldsSpy = mockSearchSource.removeField.withArgs('fieldsFromSource'); expect(removeFieldsSpy.calledOnce).toBe(true); expect(setFieldsSpy.calledOnce).toBe(true); + expect(interceptedWarnings).toBeUndefined(); + } + ); + }); + }); + + describe('function fetchSuccessors with shard failures', function () { + const mockWarnings = [ + { + originalWarning: { + message: 'Data might be incomplete because your request timed out 1', + type: 'timed_out', + }, + }, + { + originalWarning: { + message: 'Data might be incomplete because your request timed out 2', + type: 'timed_out', + }, + }, + ]; + + beforeEach(() => { + mockSearchSource = createContextSearchSourceStub('@timestamp'); + + dataPluginMock = { + search: { + searchSource: { + createEmpty: jest.fn().mockImplementation(() => mockSearchSource), + }, + showWarnings: jest.fn((adapter, callback) => { + callback(mockWarnings[0].originalWarning, {}); + callback(mockWarnings[1].originalWarning, {}); + // plus duplicates + callback(mockWarnings[0].originalWarning, {}); + callback(mockWarnings[1].originalWarning, {}); + }), + }, + } as unknown as DataPublicPluginStart; + + fetchSuccessors = (timeValIso, timeValNr, tieBreakerField, tieBreakerValue, size) => { + const anchor = buildDataTableRecord( + { + _id: '1', + _index: 'test', + _source: { + [dataView.timeFieldName!]: timeValIso, + }, + sort: [timeValNr, tieBreakerValue], + }, + dataView, + true + ); + + return fetchSurroundingDocs( + SurrDocType.SUCCESSORS, + dataView, + anchor, + tieBreakerField, + SortDirection.desc, + size, + [], + dataPluginMock, + true, + { + ...discoverServiceMock, + data: dataPluginMock, + } + ); + }; + }); + + it('should intercept request warnings', function () { + mockSearchSource._stubHits = [ + mockSearchSource._createStubHit(MS_PER_DAY * 5000), + mockSearchSource._createStubHit(MS_PER_DAY * 4000), + mockSearchSource._createStubHit(MS_PER_DAY * 3000), + mockSearchSource._createStubHit(MS_PER_DAY * 3000 - 1), + mockSearchSource._createStubHit(MS_PER_DAY * 3000 - 2), + ]; + + return fetchSuccessors(ANCHOR_TIMESTAMP_3000, MS_PER_DAY * 3000, '_doc', 0, 3).then( + ({ rows, interceptedWarnings }) => { + expect(mockSearchSource.fetch$.calledOnce).toBe(true); + expect(rows).toEqual( + buildDataTableRecordList(mockSearchSource._stubHits.slice(-3), dataView) + ); + expect(dataPluginMock.search.showWarnings).toHaveBeenCalledTimes(1); + expect(interceptedWarnings).toEqual(mockWarnings); } ); }); diff --git a/src/plugins/discover/public/application/context/services/context.ts b/src/plugins/discover/public/application/context/services/context.ts index ce448d68f38ef..1386af851911e 100644 --- a/src/plugins/discover/public/application/context/services/context.ts +++ b/src/plugins/discover/public/application/context/services/context.ts @@ -9,12 +9,17 @@ import type { Filter } from '@kbn/es-query'; import { DataView } from '@kbn/data-views-plugin/public'; import { DataPublicPluginStart, ISearchSource } from '@kbn/data-plugin/public'; import type { DataTableRecord } from '@kbn/discover-utils/types'; +import { + removeInterceptedWarningDuplicates, + type SearchResponseInterceptedWarning, +} from '@kbn/search-response-warnings'; import { reverseSortDir, SortDirection } from '../utils/sorting'; import { convertIsoToMillis, extractNanos } from '../utils/date_conversion'; import { fetchHitsInInterval } from '../utils/fetch_hits_in_interval'; import { generateIntervals } from '../utils/generate_intervals'; import { getEsQuerySearchAfter } from '../utils/get_es_query_search_after'; import { getEsQuerySort } from '../utils/get_es_query_sort'; +import type { DiscoverServices } from '../../../build_services'; export enum SurrDocType { SUCCESSORS = 'successors', @@ -36,7 +41,9 @@ const LOOKUP_OFFSETS = [0, 1, 7, 30, 365, 10000].map((days) => days * DAY_MILLIS * @param {SortDirection} sortDir - direction of sorting * @param {number} size - number of records to retrieve * @param {Filter[]} filters - to apply in the elastic query + * @param {DataPublicPluginStart} data * @param {boolean} useNewFieldsApi + * @param {DiscoverServices} services * @returns {Promise} */ export async function fetchSurroundingDocs( @@ -48,10 +55,17 @@ export async function fetchSurroundingDocs( size: number, filters: Filter[], data: DataPublicPluginStart, - useNewFieldsApi?: boolean -): Promise { + useNewFieldsApi: boolean | undefined, + services: DiscoverServices +): Promise<{ + rows: DataTableRecord[]; + interceptedWarnings: SearchResponseInterceptedWarning[] | undefined; +}> { if (typeof anchor !== 'object' || anchor === null || !size) { - return []; + return { + rows: [], + interceptedWarnings: undefined, + }; } const timeField = dataView.timeFieldName!; const searchSource = data.search.searchSource.createEmpty(); @@ -64,10 +78,11 @@ export async function fetchSurroundingDocs( nanos !== '' ? convertIsoToMillis(anchorRaw.fields?.[timeField][0]) : anchorRaw.sort?.[0]; const intervals = generateIntervals(LOOKUP_OFFSETS, timeValueMillis as number, type, sortDir); - let documents: DataTableRecord[] = []; + let rows: DataTableRecord[] = []; + let interceptedWarnings: SearchResponseInterceptedWarning[] = []; for (const interval of intervals) { - const remainingSize = size - documents.length; + const remainingSize = size - rows.length; if (remainingSize <= 0) { break; @@ -75,7 +90,7 @@ export async function fetchSurroundingDocs( const searchAfter = getEsQuerySearchAfter( type, - documents, + rows, timeField, anchor, nanos, @@ -84,7 +99,7 @@ export async function fetchSurroundingDocs( const sort = getEsQuerySort(timeField, tieBreakerField, sortDirToApply, nanos); - const hits = await fetchHitsInInterval( + const result = await fetchHitsInInterval( searchSource, timeField, sort, @@ -93,16 +108,28 @@ export async function fetchSurroundingDocs( searchAfter, remainingSize, nanos, - anchor.raw._id + anchor.raw._id, + type, + services ); - documents = + rows = type === SurrDocType.SUCCESSORS - ? [...documents, ...hits] - : [...hits.slice().reverse(), ...documents]; + ? [...rows, ...result.rows] + : [...result.rows.slice().reverse(), ...rows]; + + if (result.interceptedWarnings) { + interceptedWarnings = + type === SurrDocType.SUCCESSORS + ? [...interceptedWarnings, ...result.interceptedWarnings] + : [...result.interceptedWarnings.slice().reverse(), ...interceptedWarnings]; + } } - return documents; + return { + rows, + interceptedWarnings: removeInterceptedWarningDuplicates(interceptedWarnings), + }; } export function updateSearchSource( diff --git a/src/plugins/discover/public/application/context/services/context_query_state.ts b/src/plugins/discover/public/application/context/services/context_query_state.ts index a640c99e71e15..0b44b036be1b3 100644 --- a/src/plugins/discover/public/application/context/services/context_query_state.ts +++ b/src/plugins/discover/public/application/context/services/context_query_state.ts @@ -7,6 +7,7 @@ */ import type { DataTableRecord } from '@kbn/discover-utils/types'; +import type { SearchResponseInterceptedWarning } from '@kbn/search-response-warnings'; export interface ContextFetchState { /** @@ -33,6 +34,21 @@ export interface ContextFetchState { * Successors fetch status */ successorsStatus: LoadingStatusEntry; + + /** + * Intercepted warnings for anchor request + */ + anchorInterceptedWarnings: SearchResponseInterceptedWarning[] | undefined; + + /** + * Intercepted warnings for predecessors request + */ + predecessorsInterceptedWarnings: SearchResponseInterceptedWarning[] | undefined; + + /** + * Intercepted warnings for successors request + */ + successorsInterceptedWarnings: SearchResponseInterceptedWarning[] | undefined; } export enum LoadingStatus { @@ -60,4 +76,7 @@ export const getInitialContextQueryState = (): ContextFetchState => ({ anchorStatus: { value: LoadingStatus.UNINITIALIZED }, predecessorsStatus: { value: LoadingStatus.UNINITIALIZED }, successorsStatus: { value: LoadingStatus.UNINITIALIZED }, + anchorInterceptedWarnings: undefined, + predecessorsInterceptedWarnings: undefined, + successorsInterceptedWarnings: undefined, }); diff --git a/src/plugins/discover/public/application/context/utils/fetch_hits_in_interval.ts b/src/plugins/discover/public/application/context/utils/fetch_hits_in_interval.ts index 115d501eed135..c6fed56de4c19 100644 --- a/src/plugins/discover/public/application/context/utils/fetch_hits_in_interval.ts +++ b/src/plugins/discover/public/application/context/utils/fetch_hits_in_interval.ts @@ -10,8 +10,16 @@ import { ISearchSource, EsQuerySortValue, SortDirection } from '@kbn/data-plugin import { EsQuerySearchAfter } from '@kbn/data-plugin/common'; import { buildDataTableRecord } from '@kbn/discover-utils'; import type { DataTableRecord } from '@kbn/discover-utils/types'; +import { + getSearchResponseInterceptedWarnings, + type SearchResponseInterceptedWarning, +} from '@kbn/search-response-warnings'; +import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { convertTimeValueToIso } from './date_conversion'; import { IntervalValue } from './generate_intervals'; +import { DISABLE_SHARD_FAILURE_WARNING } from '../../../../common/constants'; +import type { SurrDocType } from '../services/context'; +import type { DiscoverServices } from '../../../build_services'; interface RangeQuery { format: string; @@ -35,8 +43,13 @@ export async function fetchHitsInInterval( searchAfter: EsQuerySearchAfter, maxCount: number, nanosValue: string, - anchorId: string -): Promise { + anchorId: string, + type: SurrDocType, + services: DiscoverServices +): Promise<{ + rows: DataTableRecord[]; + interceptedWarnings: SearchResponseInterceptedWarning[] | undefined; +}> { const range: RangeQuery = { format: 'strict_date_optional_time', }; @@ -49,6 +62,8 @@ export async function fetchHitsInInterval( if (stop) { range[sortDir === SortDirection.asc ? 'lte' : 'gte'] = convertTimeValueToIso(stop, nanosValue); } + + const adapter = new RequestAdapter(); const fetch$ = searchSource .setField('size', maxCount) .setField('query', { @@ -75,11 +90,26 @@ export async function fetchHitsInInterval( .setField('searchAfter', searchAfter) .setField('sort', sort) .setField('version', true) - .fetch$(); + .fetch$({ + disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING, + inspector: { + adapter, + title: type, + }, + }); const { rawResponse } = await lastValueFrom(fetch$); const dataView = searchSource.getField('index'); - const records = rawResponse.hits?.hits.map((hit) => buildDataTableRecord(hit, dataView!)); + const rows = rawResponse.hits?.hits.map((hit) => buildDataTableRecord(hit, dataView!)); - return records ?? []; + return { + rows: rows ?? [], + interceptedWarnings: getSearchResponseInterceptedWarnings({ + services, + adapter, + options: { + disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING, + }, + }), + }; } diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx index 24b0b6ba9fb89..4b2b7aa8e7125 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx @@ -20,6 +20,7 @@ import { DataView } from '@kbn/data-views-plugin/public'; import { SortOrder } from '@kbn/saved-search-plugin/public'; import { CellActionsProvider } from '@kbn/cell-actions'; import type { DataTableRecord } from '@kbn/discover-utils/types'; +import { SearchResponseWarnings } from '@kbn/search-response-warnings'; import { DOC_HIDE_TIME_COLUMN_SETTING, DOC_TABLE_LEGACY, @@ -203,6 +204,13 @@ function DiscoverDocumentsComponent({ + {!!documentState.interceptedWarnings?.length && ( + + )} {isLegacy && rows && rows.length && ( <> {!hideAnnouncements && } diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx index e1172a0e869d5..328153b6eeec5 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx @@ -74,6 +74,7 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) { spaces, inspector, } = useDiscoverServices(); + const globalQueryState = data.query.getState(); const { main$ } = stateContainer.dataState.data$; const [query, savedQuery, columns, sort] = useAppStateSelector((state) => [ state.query, @@ -195,20 +196,6 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) { }, [onAddColumn, draggingFieldName, currentColumns]); const mainDisplay = useMemo(() => { - if (resultState === 'none') { - const globalQueryState = data.query.getState(); - - return ( - - ); - } - if (resultState === 'uninitialized') { addLog('[DiscoverLayout] uninitialized triggers data fetching'); return stateContainer.dataState.fetch()} />; @@ -232,12 +219,9 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) { ); }, [ currentColumns, - data, dataView, isPlainRecord, - isTimeBased, onAddFilter, - onDisableFilters, onFieldEdited, resultState, stateContainer, @@ -316,14 +300,25 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) { - {resultState === 'none' && dataState.error ? ( - + {resultState === 'none' ? ( + dataState.error ? ( + + ) : ( + + ) ) : ( ({ rawResponse: { @@ -35,9 +36,13 @@ jest.spyOn(RxApi, 'lastValueFrom').mockImplementation(async () => ({ })); async function mountAndFindSubjects( - props: Omit + props: Omit< + DiscoverNoResultsProps, + 'onDisableFilters' | 'data' | 'isTimeBased' | 'stateContainer' + > ) { const services = createDiscoverServicesMock(); + const isTimeBased = props.dataView.isTimeBased(); let component: ReactWrapper; @@ -45,7 +50,8 @@ async function mountAndFindSubjects( component = await mountWithIntl( {}} {...props} /> diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results.tsx index bd010502df149..86f73e18ca4d0 100644 --- a/src/plugins/discover/public/application/main/components/no_results/no_results.tsx +++ b/src/plugins/discover/public/application/main/components/no_results/no_results.tsx @@ -10,10 +10,14 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import type { DataView } from '@kbn/data-views-plugin/common'; import type { AggregateQuery, Filter, Query } from '@kbn/es-query'; +import { SearchResponseWarnings } from '@kbn/search-response-warnings'; import { NoResultsSuggestions } from './no_results_suggestions'; +import type { DiscoverStateContainer } from '../../services/discover_state'; +import { useDataState } from '../../hooks/use_data_state'; import './_no_results.scss'; export interface DiscoverNoResultsProps { + stateContainer: DiscoverStateContainer; isTimeBased?: boolean; query: Query | AggregateQuery | undefined; filters: Filter[] | undefined; @@ -22,12 +26,26 @@ export interface DiscoverNoResultsProps { } export function DiscoverNoResults({ + stateContainer, isTimeBased, query, filters, dataView, onDisableFilters, }: DiscoverNoResultsProps) { + const { documents$ } = stateContainer.dataState.data$; + const interceptedWarnings = useDataState(documents$).interceptedWarnings; + + if (interceptedWarnings?.length) { + return ( + + ); + } + return ( diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestions.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestions.tsx index c55e8de773942..633f082c4792b 100644 --- a/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestions.tsx +++ b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestions.tsx @@ -121,6 +121,7 @@ export const NoResultsSuggestions: React.FC = ({ layout="horizontal" color="plain" icon={} + hasBorder title={

{ + .then(({ records, textBasedQueryColumns, interceptedWarnings }) => { if (services.analytics) { const duration = window.performance.now() - startTime; reportPerformanceMetricEvent(services.analytics, { @@ -131,6 +131,7 @@ export function fetchAll( fetchStatus, result: records, textBasedQueryColumns, + interceptedWarnings, recordRawType, query, }); diff --git a/src/plugins/discover/public/application/main/utils/fetch_documents.ts b/src/plugins/discover/public/application/main/utils/fetch_documents.ts index 4a4e388a27367..bce5f266d6def 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_documents.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_documents.ts @@ -11,7 +11,9 @@ import { lastValueFrom } from 'rxjs'; import { isCompleteResponse, ISearchSource } from '@kbn/data-plugin/public'; import { SAMPLE_SIZE_SETTING, buildDataTableRecordList } from '@kbn/discover-utils'; import type { EsHitRecord } from '@kbn/discover-utils/types'; +import { getSearchResponseInterceptedWarnings } from '@kbn/search-response-warnings'; import type { RecordsFetchResponse } from '../../../types'; +import { DISABLE_SHARD_FAILURE_WARNING } from '../../../../common/constants'; import { FetchDeps } from './fetch_all'; /** @@ -53,6 +55,7 @@ export const fetchDocuments = ( }), }, executionContext, + disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING, }) .pipe( filter((res) => isCompleteResponse(res)), @@ -61,5 +64,21 @@ export const fetchDocuments = ( }) ); - return lastValueFrom(fetch$).then((records) => ({ records })); + return lastValueFrom(fetch$).then((records) => { + const adapter = inspectorAdapters.requests; + const interceptedWarnings = adapter + ? getSearchResponseInterceptedWarnings({ + services, + adapter, + options: { + disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING, + }, + }) + : []; + + return { + records, + interceptedWarnings, + }; + }); }; diff --git a/src/plugins/discover/public/components/common/error_callout.tsx b/src/plugins/discover/public/components/common/error_callout.tsx index 0f1fbb722bf82..d0e914a81e851 100644 --- a/src/plugins/discover/public/components/common/error_callout.tsx +++ b/src/plugins/discover/public/components/common/error_callout.tsx @@ -10,9 +10,6 @@ import { EuiButton, EuiCallOut, EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, EuiLink, EuiModal, EuiModalBody, @@ -104,36 +101,31 @@ export const ErrorCallout = ({ /> ) : ( - - - - -

{formattedTitle}

-
- - } + title={

{formattedTitle}

} body={ - overrideDisplay?.body ?? ( - <> -

- {error.message} -

- {showErrorMessage} - - ) +
+ {overrideDisplay?.body ?? ( + <> +

+ {error.message} +

+ {showErrorMessage} + + )} +
} - css={css` - text-align: left; - `} data-test-subj={dataTestSubj} /> )} diff --git a/src/plugins/discover/public/components/doc_table/create_doc_table_embeddable.tsx b/src/plugins/discover/public/components/doc_table/create_doc_table_embeddable.tsx index e45faec8cbaa1..570c980e649e5 100644 --- a/src/plugins/discover/public/components/doc_table/create_doc_table_embeddable.tsx +++ b/src/plugins/discover/public/components/doc_table/create_doc_table_embeddable.tsx @@ -33,6 +33,7 @@ export function DiscoverDocTableEmbeddable(renderProps: DocTableEmbeddableProps) sharedItemTitle={renderProps.sharedItemTitle} isLoading={renderProps.isLoading} isPlainRecord={renderProps.isPlainRecord} + interceptedWarnings={renderProps.interceptedWarnings} dataTestSubj="embeddedSavedSearchDocTable" DocViewer={DocViewer} /> diff --git a/src/plugins/discover/public/components/doc_table/doc_table_embeddable.tsx b/src/plugins/discover/public/components/doc_table/doc_table_embeddable.tsx index 97ed5f3af9d14..6901df855984e 100644 --- a/src/plugins/discover/public/components/doc_table/doc_table_embeddable.tsx +++ b/src/plugins/discover/public/components/doc_table/doc_table_embeddable.tsx @@ -11,6 +11,7 @@ import './index.scss'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiText } from '@elastic/eui'; import { SAMPLE_SIZE_SETTING, usePager } from '@kbn/discover-utils'; +import type { SearchResponseInterceptedWarning } from '@kbn/search-response-warnings'; import { ToolBarPagination, MAX_ROWS_PER_PAGE_OPTION, @@ -22,6 +23,7 @@ import { SavedSearchEmbeddableBase } from '../../embeddable/saved_search_embedda export interface DocTableEmbeddableProps extends DocTableProps { totalHitCount: number; rowsPerPageState?: number; + interceptedWarnings?: SearchResponseInterceptedWarning[]; onUpdateRowsPerPage?: (rowsPerPage?: number) => void; } @@ -101,6 +103,7 @@ export const DocTableEmbeddable = (props: DocTableEmbeddableProps) => { return ( & filter?: (field: DataViewField, value: string[], operator: string) => void; hits?: DataTableRecord[]; totalHitCount?: number; + interceptedWarnings?: SearchResponseInterceptedWarning[]; onMoveColumn?: (column: string, index: number) => void; onUpdateRowHeight?: (rowHeight?: number) => void; onUpdateRowsPerPage?: (rowsPerPage?: number) => void; @@ -279,6 +284,7 @@ export class SavedSearchEmbeddable this.inspectorAdapters.requests!.reset(); searchProps.isLoading = true; + searchProps.interceptedWarnings = undefined; const wasAlreadyRendered = this.getOutput().rendered; @@ -357,9 +363,20 @@ export class SavedSearchEmbeddable }), }, executionContext, + disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING, }) ); + if (this.inspectorAdapters.requests) { + searchProps.interceptedWarnings = getSearchResponseInterceptedWarnings({ + services: this.services, + adapter: this.inspectorAdapters.requests, + options: { + disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING, + }, + }); + } + this.updateOutput({ ...this.getOutput(), loading: false, diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable_badge.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable_badge.tsx new file mode 100644 index 0000000000000..9944adb4be33c --- /dev/null +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable_badge.tsx @@ -0,0 +1,37 @@ +/* + * 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. + */ + +/* + * 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 React from 'react'; +import { + SearchResponseWarnings, + type SearchResponseInterceptedWarning, +} from '@kbn/search-response-warnings'; + +export interface SavedSearchEmbeddableBadgeProps { + interceptedWarnings: SearchResponseInterceptedWarning[] | undefined; +} + +export const SavedSearchEmbeddableBadge: React.FC = ({ + interceptedWarnings, +}) => { + return interceptedWarnings?.length ? ( + + ) : null; +}; diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable_base.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable_base.tsx index 17785570b9487..b41c70676c754 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable_base.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable_base.tsx @@ -9,7 +9,9 @@ import React from 'react'; import { css } from '@emotion/react'; import { EuiFlexGroup, EuiFlexItem, EuiProgress } from '@elastic/eui'; +import type { SearchResponseInterceptedWarning } from '@kbn/search-response-warnings'; import { TotalDocuments } from '../application/main/components/total_documents/total_documents'; +import { SavedSearchEmbeddableBadge } from './saved_search_embeddable_badge'; const containerStyles = css` width: 100%; @@ -22,6 +24,7 @@ export interface SavedSearchEmbeddableBaseProps { prepend?: React.ReactElement; append?: React.ReactElement; dataTestSubj?: string; + interceptedWarnings?: SearchResponseInterceptedWarning[]; } export const SavedSearchEmbeddableBase: React.FC = ({ @@ -30,6 +33,7 @@ export const SavedSearchEmbeddableBase: React.FC prepend, append, dataTestSubj, + interceptedWarnings, children, }) => { return ( @@ -62,6 +66,12 @@ export const SavedSearchEmbeddableBase: React.FC {children} {Boolean(append) && {append}} + + {Boolean(interceptedWarnings?.length) && ( +
+ +
+ )} ); }; diff --git a/src/plugins/discover/public/embeddable/saved_search_grid.tsx b/src/plugins/discover/public/embeddable/saved_search_grid.tsx index 075a3ca930235..87258347b474e 100644 --- a/src/plugins/discover/public/embeddable/saved_search_grid.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_grid.tsx @@ -7,6 +7,7 @@ */ import React, { useState, memo } from 'react'; import type { DataTableRecord } from '@kbn/discover-utils/types'; +import type { SearchResponseInterceptedWarning } from '@kbn/search-response-warnings'; import { DiscoverGrid, DiscoverGridProps } from '../components/discover_grid/discover_grid'; import './saved_search_grid.scss'; import { DiscoverGridFlyout } from '../components/discover_grid/discover_grid_flyout'; @@ -14,11 +15,13 @@ import { SavedSearchEmbeddableBase } from './saved_search_embeddable_base'; export interface DiscoverGridEmbeddableProps extends DiscoverGridProps { totalHitCount: number; + interceptedWarnings?: SearchResponseInterceptedWarning[]; } export const DataGridMemoized = memo(DiscoverGrid); export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) { + const { interceptedWarnings, ...gridProps } = props; const [expandedDoc, setExpandedDoc] = useState(undefined); return ( @@ -26,9 +29,10 @@ export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) { totalHitCount={props.totalHitCount} isLoading={props.isLoading} dataTestSubj="embeddedSavedSearchDocTable" + interceptedWarnings={props.interceptedWarnings} > { const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent; const onLoad = component.find(embeddable).props().onLoad; const adapters = createDefaultInspectorAdapters(); + adapters.tables.tables.unifiedHistogram = { meta: { statistics: { totalCount: 100 } } } as any; const rawResponse = { _shards: { total: 1, @@ -215,14 +216,21 @@ describe('Histogram', () => { failed: 1, failures: [], }, + hits: { + total: 100, + max_score: null, + hits: [], + }, }; jest .spyOn(adapters.requests, 'getRequests') .mockReturnValue([{ response: { json: { rawResponse } } } as any]); - onLoad(false, adapters); + act(() => { + onLoad(false, adapters); + }); expect(props.onTotalHitsChange).toHaveBeenLastCalledWith( - UnifiedHistogramFetchStatus.error, - undefined + UnifiedHistogramFetchStatus.complete, + 100 ); expect(props.onChartLoad).toHaveBeenLastCalledWith({ adapters }); }); diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index 9983f2e0841dd..761e701e8f9a6 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -101,10 +101,10 @@ export function Histogram({ | undefined; const response = json?.rawResponse; - // Lens will swallow shard failures and return `isLoading: false` because it displays - // its own errors, but this causes us to emit onTotalHitsChange(UnifiedHistogramFetchStatus.complete, 0). - // This is incorrect, so we check for request failures and shard failures here, and emit an error instead. - if (requestFailed || response?._shards.failed) { + // The response can have `response?._shards.failed` but we should still be able to show hits number + // TODO: show shards warnings as a badge next to the total hits number + + if (requestFailed) { onTotalHitsChange?.(UnifiedHistogramFetchStatus.error, undefined); onChartLoad?.({ adapters: adapters ?? {} }); return; diff --git a/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.ts b/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.ts index 6903cdf6b4256..c260d3171697b 100644 --- a/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.ts +++ b/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.ts @@ -206,6 +206,7 @@ const fetchTotalHitsSearchSource = async ({ executionContext: { description: 'fetch total hits', }, + disableShardFailureWarning: true, // TODO: show warnings as a badge next to total hits number }) .pipe( filter((res) => isCompleteResponse(res)), diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index f03af110fd866..66a2e385d3e6c 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -239,7 +239,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.graph.savePolicy (alternatives)', 'xpack.ilm.ui.enabled (boolean)', 'xpack.index_management.ui.enabled (boolean)', - 'xpack.index_management.enableIndexActions (boolean)', + 'xpack.index_management.enableIndexActions (any)', 'xpack.infra.sources.default.fields.message (array)', /** * xpack.infra.logs is conditional and will resolve to an object of properties diff --git a/tsconfig.base.json b/tsconfig.base.json index 12504320663e3..8efe5c62421de 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1172,6 +1172,8 @@ "@kbn/screenshotting-plugin/*": ["x-pack/plugins/screenshotting/*"], "@kbn/search-examples-plugin": ["examples/search_examples"], "@kbn/search-examples-plugin/*": ["examples/search_examples/*"], + "@kbn/search-response-warnings": ["packages/kbn-search-response-warnings"], + "@kbn/search-response-warnings/*": ["packages/kbn-search-response-warnings/*"], "@kbn/searchprofiler-plugin": ["x-pack/plugins/searchprofiler"], "@kbn/searchprofiler-plugin/*": ["x-pack/plugins/searchprofiler/*"], "@kbn/security-api-integration-helpers": ["x-pack/test/security_api_integration/packages/helpers"], diff --git a/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts b/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts index 5c4acce28b108..e3942b26ee6fa 100644 --- a/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts +++ b/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts @@ -114,7 +114,6 @@ const getIndexTemplatePutBody = (opts?: GetIndexTemplatePutBodyOpts) => { name: '.alerts-ilm-policy', rollover_alias: `.alerts-${context ? context : 'test'}.alerts-${namespace}`, }, - 'index.mapping.ignore_malformed': true, 'index.mapping.total_fields.limit': 2500, }, mappings: { @@ -641,7 +640,6 @@ describe('Alerts Service', () => { name: '.alerts-ilm-policy', rollover_alias: `.alerts-empty.alerts-default`, }, - 'index.mapping.ignore_malformed': true, 'index.mapping.total_fields.limit': 2500, }, mappings: { diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.test.ts b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.test.ts index 38c2207e5f410..d4ce203a0d0e3 100644 --- a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.test.ts +++ b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.test.ts @@ -42,7 +42,6 @@ const IndexTemplate = (namespace: string = 'default') => ({ name: 'test-ilm-policy', rollover_alias: `.alerts-test.alerts-${namespace}`, }, - 'index.mapping.ignore_malformed': true, 'index.mapping.total_fields.limit': 2500, }, }, diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.ts b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.ts index 388fe6344a51f..a17fad2d875ed 100644 --- a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.ts +++ b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.ts @@ -54,7 +54,6 @@ export const getIndexTemplate = ({ rollover_alias: indexPatterns.alias, }, 'index.mapping.total_fields.limit': totalFieldsLimit, - 'index.mapping.ignore_malformed': true, }, mappings: { dynamic: false, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx index c99bfd21c9ad1..51c6cdc54131d 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx @@ -9,7 +9,7 @@ import React, { FC } from 'react'; import useObservable from 'react-use/lib/useObservable'; import ReactDOM from 'react-dom'; import { CoreStart } from '@kbn/core/public'; -import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { IEmbeddable, EmbeddableFactory, @@ -62,7 +62,7 @@ const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => { style={{ width: '100%', height: '100%', cursor: 'auto' }} > - + diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/index.tsx index 12e1948260a96..2a875ff88f413 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { StartInitializer } from '../../../plugin'; import { RendererFactory } from '../../../../types'; import { AdvancedFilter } from './component'; @@ -24,7 +24,7 @@ export const advancedFilterFactory: StartInitializer> = height: 50, render(domNode, _, handlers) { ReactDOM.render( - + handlers.event({ name: 'applyFilterAction', data: filter })} value={handlers.getFilter()} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx index 76323793ab698..50f534a658359 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx @@ -9,7 +9,7 @@ import { fromExpression, toExpression, Ast } from '@kbn/interpreter'; import { get } from 'lodash'; import React from 'react'; import ReactDOM from 'react-dom'; -import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { syncFilterExpression } from '../../../../public/lib/sync_filter_expression'; import { RendererFactory } from '../../../../types'; import { StartInitializer } from '../../../plugin'; @@ -97,7 +97,7 @@ export const dropdownFilterFactory: StartInitializer> = ); ReactDOM.render( - {filter}, + {filter}, domNode, () => handlers.done() ); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx index 9ae72a82c8870..52ae1e28f7904 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx @@ -9,7 +9,7 @@ import ReactDOM from 'react-dom'; import React from 'react'; import { toExpression } from '@kbn/interpreter'; import { UI_SETTINGS } from '@kbn/data-plugin/public'; -import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { syncFilterExpression } from '../../../../public/lib/sync_filter_expression'; import { RendererStrings } from '../../../../i18n'; import { TimeFilter } from './components'; @@ -60,7 +60,7 @@ export const timeFilterFactory: StartInitializer> = ( } ReactDOM.render( - + handlers.event({ name: 'applyFilterAction', data: filter })} filter={filterExpression} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/index.tsx index 7566db85427ac..fd1e2415d0589 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/index.tsx @@ -9,7 +9,7 @@ import React, { CSSProperties } from 'react'; import ReactDOM from 'react-dom'; import { CoreTheme } from '@kbn/core/public'; import { Observable } from 'rxjs'; -import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { defaultTheme$ } from '@kbn/presentation-util-plugin/common'; import { Markdown } from '@kbn/kibana-react-plugin/public'; import { StartInitializer } from '../../plugin'; @@ -30,7 +30,7 @@ export const getMarkdownRenderer = const fontStyle = config.font ? config.font.spec : {}; ReactDOM.render( - + +
+
{textString}
, domNode, diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx index cc365451b46f0..fa544095a8598 100644 --- a/x-pack/plugins/canvas/public/application.tsx +++ b/x-pack/plugins/canvas/public/application.tsx @@ -17,7 +17,8 @@ import { includes, remove } from 'lodash'; import { AppMountParameters, CoreStart, CoreSetup, AppUpdater } from '@kbn/core/public'; -import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { PluginServices } from '@kbn/presentation-util-plugin/public'; import { CanvasStartDeps, CanvasSetupDeps } from './plugin'; @@ -75,7 +76,7 @@ export const renderApp = ({ - + @@ -151,7 +152,7 @@ export const initializeCanvas = async ( ], content: (domNode, { hideHelpMenu }) => { ReactDOM.render( - + diff --git a/x-pack/plugins/canvas/public/components/home/home.component.tsx b/x-pack/plugins/canvas/public/components/home/home.component.tsx index dbfbd8a920c7e..c29713da70d11 100644 --- a/x-pack/plugins/canvas/public/components/home/home.component.tsx +++ b/x-pack/plugins/canvas/public/components/home/home.component.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { KibanaPageTemplate } from '@kbn/kibana-react-plugin/public'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { withSuspense } from '@kbn/presentation-util-plugin/public'; import { WorkpadCreate } from './workpad_create'; @@ -48,7 +48,9 @@ export const Home = ({ activeTab = 'workpads' }: Props) => { ], }} > - {tab === 'workpads' ? : } + + {tab === 'workpads' ? : } + ); }; diff --git a/x-pack/plugins/canvas/tsconfig.json b/x-pack/plugins/canvas/tsconfig.json index 04d1e78fc9555..45b22e422dc6e 100644 --- a/x-pack/plugins/canvas/tsconfig.json +++ b/x-pack/plugins/canvas/tsconfig.json @@ -82,6 +82,8 @@ "@kbn/core-saved-objects-server", "@kbn/discover-utils", "@kbn/content-management-plugin", + "@kbn/react-kibana-context-theme", + "@kbn/shared-ux-page-kibana-template", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/comment.test.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/comment.test.tsx index 77d38e87a2c41..bab441edbe80c 100644 --- a/x-pack/plugins/cases/public/components/user_actions/comment/comment.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/comment.test.tsx @@ -587,28 +587,60 @@ describe('createCommentUserActionBuilder', () => { }); }); - it('renders correctly an action', async () => { - const userAction = getHostIsolationUserAction(); - - const builder = createCommentUserActionBuilder({ - ...builderArgs, - caseData: { - ...builderArgs.caseData, - comments: [hostIsolationComment()], - }, - userAction, + describe('Host isolation action', () => { + it('renders correctly an action', async () => { + const userAction = getHostIsolationUserAction(); + + const builder = createCommentUserActionBuilder({ + ...builderArgs, + caseData: { + ...builderArgs.caseData, + comments: [hostIsolationComment()], + }, + userAction, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + expect(screen.getByTestId('endpoint-action')).toBeInTheDocument(); + expect(screen.getByText('submitted isolate request on host')).toBeInTheDocument(); + expect(screen.getByText('host1')).toBeInTheDocument(); + expect(screen.getByText('I just isolated the host!')).toBeInTheDocument(); }); - const createdUserAction = builder.build(); - render( - - - - ); + it('shows the correct username', async () => { + const createdBy = { profileUid: userProfiles[0].uid }; + const userAction = getHostIsolationUserAction({ + createdBy, + }); - expect(screen.getByText('submitted isolate request on host')).toBeInTheDocument(); - expect(screen.getByText('host1')).toBeInTheDocument(); - expect(screen.getByText('I just isolated the host!')).toBeInTheDocument(); + const builder = createCommentUserActionBuilder({ + ...builderArgs, + caseData: { + ...builderArgs.caseData, + comments: [hostIsolationComment({ createdBy })], + }, + userAction, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + expect( + screen.getAllByTestId('case-user-profile-avatar-damaged_raccoon')[0] + ).toBeInTheDocument(); + expect(screen.getAllByText('DR')[0]).toBeInTheDocument(); + expect(screen.getAllByText('Damaged Raccoon')[0]).toBeInTheDocument(); + }); }); describe('Attachment framework', () => { diff --git a/x-pack/plugins/cases/public/components/user_actions/index.test.tsx b/x-pack/plugins/cases/public/components/user_actions/index.test.tsx index 15c2a8661edb0..4530e115a2f04 100644 --- a/x-pack/plugins/cases/public/components/user_actions/index.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/index.test.tsx @@ -13,18 +13,11 @@ import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; import routeData from 'react-router'; import { useUpdateComment } from '../../containers/use_update_comment'; -import { - basicCase, - caseUserActions, - getHostIsolationUserAction, - getUserAction, - hostIsolationComment, -} from '../../containers/mock'; +import { basicCase, caseUserActions, getUserAction } from '../../containers/mock'; import { UserActions } from '.'; import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer } from '../../common/mock'; import { UserActionActions } from '../../../common/types/domain'; -import { userProfiles, userProfilesMap } from '../../containers/user_profiles/api.mock'; import { getCaseConnectorsMockResponse } from '../../common/mock/connectors'; import type { UserActivityParams } from '../user_actions_activity_bar/types'; import { useFindCaseUserActions } from '../../containers/use_find_case_user_actions'; @@ -325,52 +318,4 @@ describe.skip(`UserActions`, () => { expect(screen.getAllByTestId('add-comment')[1].textContent).toContain(newComment); }); }); - - // FLAKY: https://github.com/elastic/kibana/issues/156742 - describe.skip('Host isolation action', () => { - it('renders in the cases details view', async () => { - const isolateAction = [getHostIsolationUserAction()]; - const props = { - ...defaultProps, - data: { ...defaultProps.data, comments: [...basicCase.comments, hostIsolationComment()] }, - }; - - useFindCaseUserActionsMock.mockReturnValue({ - ...defaultUseFindCaseUserActions, - data: { userActions: isolateAction }, - }); - - appMockRender.render(); - await waitFor(() => { - expect(screen.getByTestId('endpoint-action')).toBeInTheDocument(); - }); - }); - - it('shows the correct username', async () => { - const isolateAction = [ - getHostIsolationUserAction({ createdBy: { profileUid: userProfiles[0].uid } }), - ]; - const props = { - ...defaultProps, - userProfiles: userProfilesMap, - data: { - ...defaultProps.data, - comments: [hostIsolationComment({ createdBy: { profileUid: userProfiles[0].uid } })], - }, - }; - - useFindCaseUserActionsMock.mockReturnValue({ - ...defaultUseFindCaseUserActions, - data: { userActions: isolateAction }, - }); - - appMockRender.render(); - - expect( - screen.getAllByTestId('case-user-profile-avatar-damaged_raccoon')[0] - ).toBeInTheDocument(); - expect(screen.getAllByText('DR')[0]).toBeInTheDocument(); - expect(screen.getAllByText('Damaged Raccoon')[0]).toBeInTheDocument(); - }); - }); }); diff --git a/x-pack/plugins/cloud/README.md b/x-pack/plugins/cloud/README.md index 0b9a75de7e030..00aa160fb3600 100644 --- a/x-pack/plugins/cloud/README.md +++ b/x-pack/plugins/cloud/README.md @@ -1,65 +1,3 @@ # `cloud` plugin -The `cloud` plugin adds Cloud-specific features to Kibana. - -## Client-side API - -The client-side plugin provides the following interface. - -### `isCloudEnabled` - -This is set to `true` for both ESS and ECE deployments. - -### `cloudId` - -This is the ID of the Cloud deployment to which the Kibana instance belongs. - -**Example:** `eastus2.azure.elastic-cloud.com:9243$59ef636c6917463db140321484d63cfa$a8b109c08adc43279ef48f29af1a3911` - -**NOTE:** The `cloudId` is a concatenation of the deployment name and a hash. Users can update the deployment name, changing the `cloudId`. However, the changed `cloudId` will not be re-injected into `kibana.yml`. If you need the current `cloudId` the best approach is to split the injected `cloudId` on the semi-colon, and replace the first element with the `persistent.cluster.metadata.display_name` value as provided by a call to `GET _cluster/settings`. - -### `baseUrl` - -This is the URL of the Cloud interface. - -**Example:** `https://cloud.elastic.co` (on the ESS production environment) - -### `deploymentUrl` - -This is the path to the Cloud deployment management page for the deployment to which the Kibana instance belongs. The value is already prepended with `baseUrl`. - -**Example:** `{baseUrl}/deployments/bfdad4ef99a24212a06d387593686d63` - -### `snapshotsUrl` - -This is the path to the Snapshots page for the deployment to which the Kibana instance belongs. The value is already prepended with `deploymentUrl`. - -**Example:** `{deploymentUrl}/elasticsearch/snapshots` - -### `profileUrl` - -This is the path to the Cloud User Profile page. The value is already prepended with `baseUrl`. - -**Example:** `{baseUrl}/user/settings/` - -### `organizationUrl` - -This is the path to the Cloud Account and Billing page. The value is already prepended with `baseUrl`. - -**Example:** `{baseUrl}/account/` - -### `cname` - -This value is the same as `baseUrl` on ESS but can be customized on ECE. - -**Example:** `cloud.elastic.co` (on ESS) - -### `trial_end_date` - -The end date for the Elastic Cloud trial. Only available on Elastic Cloud. - -**Example:** `2020-10-14T10:40:22Z` - -### `is_elastic_staff_owned` - -`true` if the deployment is owned by an Elastician. Only available on Elastic Cloud. \ No newline at end of file +The `cloud` plugin adds Cloud-specific features to Kibana. \ No newline at end of file diff --git a/x-pack/plugins/cloud/public/utils.ts b/x-pack/plugins/cloud/common/utils.ts similarity index 100% rename from x-pack/plugins/cloud/public/utils.ts rename to x-pack/plugins/cloud/common/utils.ts diff --git a/x-pack/plugins/cloud/public/plugin.tsx b/x-pack/plugins/cloud/public/plugin.tsx index 60e6d7a1af08f..f0fad877a1bd5 100644 --- a/x-pack/plugins/cloud/public/plugin.tsx +++ b/x-pack/plugins/cloud/public/plugin.tsx @@ -14,7 +14,7 @@ import { parseDeploymentIdFromDeploymentUrl } from '../common/parse_deployment_i import { ELASTIC_SUPPORT_LINK, CLOUD_SNAPSHOTS_PATH } from '../common/constants'; import { decodeCloudId, type DecodedCloudId } from '../common/decode_cloud_id'; import type { CloudSetup, CloudStart } from './types'; -import { getFullCloudUrl } from './utils'; +import { getFullCloudUrl } from '../common/utils'; export interface CloudConfigType { id?: string; diff --git a/x-pack/plugins/cloud/public/types.ts b/x-pack/plugins/cloud/public/types.ts index f6dfc71c6dfb2..a42730905c9c7 100644 --- a/x-pack/plugins/cloud/public/types.ts +++ b/x-pack/plugins/cloud/public/types.ts @@ -21,7 +21,9 @@ export interface CloudStart { */ cloudId?: string; /** - * The full URL to the deployment management page on Elastic Cloud. Undefined if not running on Cloud. + * This is the path to the Cloud deployment management page for the deployment to which the Kibana instance belongs. The value is already prepended with `baseUrl`. + * + * @example `{baseUrl}/deployments/bfdad4ef99a24212a06d387593686d63` */ deploymentUrl?: string; /** @@ -84,6 +86,8 @@ export interface CloudSetup { deploymentId?: string; /** * This value is the same as `baseUrl` on ESS but can be customized on ECE. + * + * @example `cloud.elastic.co` */ cname?: string; /** @@ -92,6 +96,8 @@ export interface CloudSetup { baseUrl?: string; /** * The full URL to the deployment management page on Elastic Cloud. Undefined if not running on Cloud. + * + * @example `{baseUrl}/deployments/bfdad4ef99a24212a06d387593686d63` */ deploymentUrl?: string; /** @@ -99,15 +105,21 @@ export interface CloudSetup { */ projectsUrl?: string; /** - * The full URL to the user profile page on Elastic Cloud. Undefined if not running on Cloud. + * This is the path to the Cloud User Profile page. The value is already prepended with `baseUrl`. + * + * @example `{baseUrl}/user/settings/` */ profileUrl?: string; /** - * The full URL to the organization management page on Elastic Cloud. Undefined if not running on Cloud. + * This is the path to the Cloud Account and Billing page. The value is already prepended with `baseUrl`. + * + * @example `{baseUrl}/account/` */ organizationUrl?: string; /** * This is the path to the Snapshots page for the deployment to which the Kibana instance belongs. The value is already prepended with `deploymentUrl`. + * + * @example `{deploymentUrl}/elasticsearch/snapshots` */ snapshotsUrl?: string; /** @@ -131,7 +143,9 @@ export interface CloudSetup { */ isCloudEnabled: boolean; /** - * When the Cloud Trial ends/ended for the organization that owns this deployment. Only available when running on Elastic Cloud. + * The end date for the Elastic Cloud trial. Only available on Elastic Cloud. + * + * @example `2020-10-14T10:40:22Z` */ trialEndDate?: Date; /** @@ -140,6 +154,7 @@ export interface CloudSetup { isElasticStaffOwned?: boolean; /** * Registers CloudServiceProviders so start's `CloudContextProvider` hooks them. + * * @param contextProvider The React component from the Service Provider. */ registerCloudService: (contextProvider: FC) => void; diff --git a/x-pack/plugins/cloud/server/__snapshots__/plugin.test.ts.snap b/x-pack/plugins/cloud/server/__snapshots__/plugin.test.ts.snap new file mode 100644 index 0000000000000..505c66345d77b --- /dev/null +++ b/x-pack/plugins/cloud/server/__snapshots__/plugin.test.ts.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Cloud Plugin #setup interface snapshot 1`] = ` +Object { + "apm": Object { + "secretToken": undefined, + "url": undefined, + }, + "baseUrl": "https://cloud.elastic.co", + "cloudDefaultPort": undefined, + "cloudHost": undefined, + "cloudId": "cloudId", + "deploymentId": "deployment-id", + "elasticsearchUrl": undefined, + "instanceSizeMb": undefined, + "isCloudEnabled": true, + "isElasticStaffOwned": undefined, + "isServerlessEnabled": false, + "kibanaUrl": undefined, + "projectsUrl": "https://cloud.elastic.co/projects/", + "serverless": Object { + "projectId": undefined, + }, + "trialEndDate": undefined, +} +`; + +exports[`Cloud Plugin #start interface snapshot 1`] = ` +Object { + "baseUrl": "https://cloud.elastic.co", + "isCloudEnabled": true, + "projectsUrl": "https://cloud.elastic.co/projects/", +} +`; diff --git a/x-pack/plugins/cloud/server/mocks.ts b/x-pack/plugins/cloud/server/mocks.ts index dcb04f089a266..6f06b9639d960 100644 --- a/x-pack/plugins/cloud/server/mocks.ts +++ b/x-pack/plugins/cloud/server/mocks.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { CloudSetup } from './plugin'; +import type { CloudSetup, CloudStart } from './plugin'; function createSetupMock(): jest.Mocked { return { @@ -19,6 +19,8 @@ function createSetupMock(): jest.Mocked { isCloudEnabled: true, isElasticStaffOwned: true, trialEndDate: new Date('2020-10-01T14:13:12Z'), + projectsUrl: 'projects-url', + baseUrl: 'base-url', apm: { url: undefined, secretToken: undefined, @@ -30,6 +32,15 @@ function createSetupMock(): jest.Mocked { }; } +function createStartMock(): jest.Mocked { + return { + isCloudEnabled: true, + projectsUrl: 'projects-url', + baseUrl: 'base-url', + }; +} + export const cloudMock = { createSetup: createSetupMock, + createStart: createStartMock, }; diff --git a/x-pack/plugins/cloud/server/plugin.test.ts b/x-pack/plugins/cloud/server/plugin.test.ts index 76264cb248c17..0309271a8ea63 100644 --- a/x-pack/plugins/cloud/server/plugin.test.ts +++ b/x-pack/plugins/cloud/server/plugin.test.ts @@ -15,6 +15,7 @@ const baseConfig = { base_url: 'https://cloud.elastic.co', deployment_url: '/abc123', profile_url: '/user/settings/', + projects_url: '/projects/', organization_url: '/account/', }; @@ -42,6 +43,10 @@ describe('Cloud Plugin', () => { describe('#setup', () => { describe('interface', () => { + it('snapshot', () => { + const { setup } = setupPlugin(); + expect(setup).toMatchSnapshot(); + }); it('exposes isCloudEnabled', () => { const { setup } = setupPlugin(); expect(setup.isCloudEnabled).toBe(true); @@ -124,6 +129,10 @@ describe('Cloud Plugin', () => { describe('#start', () => { describe('interface', () => { + it('snapshot', () => { + const { start } = setupPlugin(); + expect(start).toMatchSnapshot(); + }); it('exposes isCloudEnabled', () => { const { start } = setupPlugin(); expect(start.isCloudEnabled).toBe(true); diff --git a/x-pack/plugins/cloud/server/plugin.ts b/x-pack/plugins/cloud/server/plugin.ts index ee769cf0b14c3..5838f5086fa53 100644 --- a/x-pack/plugins/cloud/server/plugin.ts +++ b/x-pack/plugins/cloud/server/plugin.ts @@ -14,6 +14,7 @@ import { registerCloudUsageCollector } from './collectors'; import { getIsCloudEnabled } from '../common/is_cloud_enabled'; import { parseDeploymentIdFromDeploymentUrl } from '../common/parse_deployment_id_from_deployment_url'; import { decodeCloudId, DecodedCloudId } from '../common/decode_cloud_id'; +import { getFullCloudUrl } from '../common/utils'; import { readInstanceSizeMb } from './env'; interface PluginsSetup { @@ -25,7 +26,11 @@ interface PluginsSetup { */ export interface CloudSetup { /** - * The deployment's Cloud ID. Only available when running on Elastic Cloud. + * This is the ID of the Cloud deployment to which the Kibana instance belongs. + * + * @example `eastus2.azure.elastic-cloud.com:9243$59ef636c6917463db140321484d63cfa$a8b109c08adc43279ef48f29af1a3911` + * + * @note The `cloudId` is a concatenation of the deployment name and a hash. Users can update the deployment name, changing the `cloudId`. However, the changed `cloudId` will not be re-injected into `kibana.yml`. If you need the current `cloudId` the best approach is to split the injected `cloudId` on the semi-colon, and replace the first element with the `persistent.cluster.metadata.display_name` value as provided by a call to `GET _cluster/settings`. */ cloudId?: string; /** @@ -41,15 +46,27 @@ export interface CloudSetup { */ kibanaUrl?: string; /** - * {host} from the deployment url https://.. + * This is the URL to the "projects" interface on cloud. + * + * @example `https://cloud.elastic.co/projects` + */ + projectsUrl?: string; + /** + * This is the URL of the Cloud interface. + * + * @example `https://cloud.elastic.co` (on the ESS production environment) + */ + baseUrl?: string; + /** + * {host} of the deployment url https://.. */ cloudHost?: string; /** - * {port} from the deployment url https://.. + * {port} of the deployment url https://.. */ cloudDefaultPort?: string; /** - * `true` when running on Elastic Cloud. + * This is set to `true` for both ESS and ECE deployments. */ isCloudEnabled: boolean; /** @@ -77,7 +94,11 @@ export interface CloudSetup { */ isServerlessEnabled: boolean; /** - * Serverless configuration + * Serverless configuration. + * + * @note We decided to place any cloud URL values at the top level of this object + * even if they contain serverless specific values. All other serverless + * config should live in this object. */ serverless: { /** @@ -93,9 +114,21 @@ export interface CloudSetup { */ export interface CloudStart { /** - * `true` when running on Elastic Cloud. + * This is set to `true` for both ESS and ECE deployments. */ isCloudEnabled: boolean; + /** + * This is the URL to the "projects" interface on cloud. + * + * @example `https://cloud.elastic.co/projects` + */ + projectsUrl?: string; + /** + * This is the URL of the Cloud interface. + * + * @example `https://cloud.elastic.co` (on the ESS production environment) + */ + baseUrl?: string; } export class CloudPlugin implements Plugin { @@ -124,6 +157,7 @@ export class CloudPlugin implements Plugin { } return { + ...this.getCloudUrls(), cloudId: this.config.id, instanceSizeMb: readInstanceSizeMb(), deploymentId: parseDeploymentIdFromDeploymentUrl(this.config.deployment_url), @@ -145,9 +179,19 @@ export class CloudPlugin implements Plugin { }; } - public start() { + public start(): CloudStart { return { + ...this.getCloudUrls(), isCloudEnabled: getIsCloudEnabled(this.config.id), }; } + + private getCloudUrls() { + const { base_url: baseUrl } = this.config; + const projectsUrl = getFullCloudUrl(baseUrl, this.config.projects_url); + return { + baseUrl, + projectsUrl, + }; + } } diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index 058b23e3477a5..2d8b8157ee758 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -123,3 +123,7 @@ export const VULNERABILITIES_SEVERITY: Record = { CRITICAL: 'CRITICAL', UNKNOWN: 'UNKNOWN', }; + +export const VULNERABILITIES_ENUMERATION = 'CVE'; +export const SETUP_ACCESS_CLOUD_SHELL = 'google_cloud_shell'; +export const SETUP_ACCESS_MANUAL = 'manual'; diff --git a/x-pack/plugins/cloud_security_posture/public/common/constants.ts b/x-pack/plugins/cloud_security_posture/public/common/constants.ts index 3ef666a1c110f..a08ebb48fdd01 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/constants.ts @@ -75,7 +75,7 @@ export const cloudPostureIntegrations: CloudPostureIntegrations = { { type: CLOUDBEAT_AWS, name: i18n.translate('xpack.csp.cspmIntegration.awsOption.nameTitle', { - defaultMessage: 'Amazon Web Services', + defaultMessage: 'AWS', }), benchmark: i18n.translate('xpack.csp.cspmIntegration.awsOption.benchmarkTitle', { defaultMessage: 'CIS AWS', diff --git a/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx index 2709163c22a84..c912a61224757 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx @@ -10,6 +10,7 @@ import { CIS_AWS, CIS_GCP } from '../../common/constants'; import { Cluster } from '../../common/types'; import { CISBenchmarkIcon } from './cis_benchmark_icon'; import { CompactFormattedNumber } from './compact_formatted_number'; +import { useNavigateFindings } from '../common/hooks/use_navigate_findings'; export const AccountsEvaluatedWidget = ({ clusters, @@ -23,6 +24,12 @@ export const AccountsEvaluatedWidget = ({ return clusters?.filter((obj) => obj?.meta.benchmark.id === benchmarkId) || []; }; + const navToFindings = useNavigateFindings(); + + const navToFindingsByCloudProvider = (provider: string) => { + navToFindings({ 'cloud.provider': provider }); + }; + const cisAwsClusterAmount = filterClustersById(CIS_AWS).length; const cisGcpClusterAmount = filterClustersById(CIS_GCP).length; @@ -32,32 +39,46 @@ export const AccountsEvaluatedWidget = ({ return ( <> - - - - - - - - - - - - - - - - - - - - + {cisAwsClusterAmount > 0 && ( + + + + + + { + navToFindingsByCloudProvider('aws'); + }} + > + + + + + )} + {cisGcpClusterAmount > 0 && ( + + + + + + { + navToFindingsByCloudProvider('gcp'); + }} + > + + + + + )} ); diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_credentials_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_credentials_form.tsx index 6dd52f259066e..4b50ccbd73c8a 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_credentials_form.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_credentials_form.tsx @@ -167,7 +167,7 @@ const Link = ({ children, url }: { children: React.ReactNode; url: string }) => ); -const ReadDocumentation = ({ url }: { url: string }) => { +export const ReadDocumentation = ({ url }: { url: string }) => { return ( ( <> - + +

( ); -/* NEED TO FIND THE REAL URL HERE LATER */ -const DocsLink = ( - - - documentation - - ), - }} - /> - -); +const GoogleCloudShellSetup = () => { + return ( + <> + +
    +
  1. + +
  2. +
  3. + +
  4. +
  5. + +
  6. +
+
+ + + ); +}; -type GcpCredentialsType = 'credentials_file' | 'credentials_json'; type GcpFields = Record; interface GcpInputFields { fields: GcpFields; } -const gcpField: GcpInputFields = { +export const gcpField: GcpInputFields = { fields: { project_id: { label: i18n.translate('xpack.csp.gcpIntegration.projectidFieldLabel', { @@ -132,14 +166,14 @@ const getSetupFormatOptions = (): Array<{ disabled: boolean; }> => [ { - id: 'google_cloud_shell', + id: SETUP_ACCESS_CLOUD_SHELL, label: i18n.translate('xpack.csp.gcpIntegration.setupFormatOptions.googleCloudShell', { defaultMessage: 'Google Cloud Shell', }), - disabled: true, + disabled: false, }, { - id: 'manual', + id: SETUP_ACCESS_MANUAL, label: i18n.translate('xpack.csp.gcpIntegration.setupFormatOptions.manual', { defaultMessage: 'Manual', }), @@ -175,6 +209,83 @@ const getInputVarsFields = ( } as const; }); +const getSetupFormatFromInput = ( + input: Extract< + NewPackagePolicyPostureInput, + { type: 'cloudbeat/cis_aws' | 'cloudbeat/cis_eks' | 'cloudbeat/cis_gcp' } + > +): SetupFormatGCP => { + const credentialsType = input.streams[0].vars?.setup_access?.value; + // Google Cloud shell is the default value + if (!credentialsType) { + return SETUP_ACCESS_CLOUD_SHELL; + } + if (credentialsType !== SETUP_ACCESS_CLOUD_SHELL) { + return SETUP_ACCESS_MANUAL; + } + + return SETUP_ACCESS_CLOUD_SHELL; +}; + +const getGoogleCloudShellUrl = (newPolicy: NewPackagePolicy) => { + const template: string | undefined = newPolicy?.inputs?.find((i) => i.type === CLOUDBEAT_GCP) + ?.config?.cloud_shell_url?.value; + + return template || undefined; +}; + +const updateCloudShellUrl = ( + newPolicy: NewPackagePolicy, + updatePolicy: (policy: NewPackagePolicy) => void, + templateUrl: string | undefined +) => { + updatePolicy?.({ + ...newPolicy, + inputs: newPolicy.inputs.map((input) => { + if (input.type === CLOUDBEAT_GCP) { + return { + ...input, + config: { cloud_shell_url: { value: templateUrl } }, + }; + } + return input; + }), + }); +}; + +const useCloudShellUrl = ({ + packageInfo, + newPolicy, + updatePolicy, + setupFormat, +}: { + packageInfo: PackageInfo; + newPolicy: NewPackagePolicy; + updatePolicy: (policy: NewPackagePolicy) => void; + setupFormat: SetupFormatGCP; +}) => { + useEffect(() => { + const policyInputCloudShellUrl = getGoogleCloudShellUrl(newPolicy); + + if (setupFormat === SETUP_ACCESS_MANUAL) { + if (!!policyInputCloudShellUrl) { + updateCloudShellUrl(newPolicy, updatePolicy, undefined); + } + return; + } + const templateUrl = getCspmCloudShellDefaultValue(packageInfo); + + // If the template is not available, do not update the policy + if (templateUrl === '') return; + + // If the template is already set, do not update the policy + if (policyInputCloudShellUrl === templateUrl) return; + + updateCloudShellUrl(newPolicy, updatePolicy, templateUrl); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [newPolicy?.vars?.cloud_shell_url, newPolicy, packageInfo, setupFormat]); +}; + export const GcpCredentialsForm = ({ input, newPolicy, @@ -187,15 +298,67 @@ export const GcpCredentialsForm = ({ const validSemantic = semverValid(packageInfo.version); const integrationVersionNumberOnly = semverCoerce(validSemantic) || ''; const isInvalid = semverLt(integrationVersionNumberOnly, MIN_VERSION_GCP_CIS); + const fieldsSnapshot = useRef({}); + const lastSetupAccessType = useRef(undefined); + const setupFormat = getSetupFormatFromInput(input); + const getFieldById = (id: keyof GcpInputFields['fields']) => { + return fields.find((element) => element.id === id); + }; + + useCloudShellUrl({ + packageInfo, + newPolicy, + updatePolicy, + setupFormat, + }); + const onSetupFormatChange = (newSetupFormat: SetupFormatGCP) => { + if (newSetupFormat === SETUP_ACCESS_CLOUD_SHELL) { + // We need to store the current manual fields to restore them later + fieldsSnapshot.current = Object.fromEntries( + fields.map((field) => [field.id, { value: field.value }]) + ); + // We need to store the last manual credentials type to restore it later + lastSetupAccessType.current = input.streams[0].vars?.setup_access?.value; + + updatePolicy( + getPosturePolicy(newPolicy, input.type, { + setup_access: { + value: SETUP_ACCESS_CLOUD_SHELL, + type: 'text', + }, + // Clearing fields from previous setup format to prevent exposing credentials + // when switching from manual to cloud formation + ...Object.fromEntries(fields.map((field) => [field.id, { value: undefined }])), + }) + ); + } else { + updatePolicy( + getPosturePolicy(newPolicy, input.type, { + setup_access: { + // Restoring last manual credentials type or defaulting to the first option + value: lastSetupAccessType.current || SETUP_ACCESS_MANUAL, + type: 'text', + }, + // Restoring fields from manual setup format if any + ...fieldsSnapshot.current, + }) + ); + } + }; + // Integration is Invalid IF Version is not at least 1.5.0 OR Setup Access is manual but Project ID is empty useEffect(() => { - setIsValid(!isInvalid); + const isProjectIdEmpty = + setupFormat === SETUP_ACCESS_MANUAL && !getFieldById('project_id')?.value; + const isInvalidPolicy = isInvalid || isProjectIdEmpty; + + setIsValid(!isInvalidPolicy); onChange({ - isValid: !isInvalid, + isValid: !isInvalidPolicy, updatedPolicy: newPolicy, }); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [input, packageInfo]); + }, [input, packageInfo, setupFormat]); if (isInvalid) { return ( @@ -214,32 +377,30 @@ export const GcpCredentialsForm = ({ <> - updatePolicy(getPosturePolicy(newPolicy, input.type))} + - - updatePolicy(getPosturePolicy(newPolicy, input.type, { [key]: { value } })) - } - /> + {setupFormat === SETUP_ACCESS_MANUAL ? ( + + updatePolicy(getPosturePolicy(newPolicy, input.type, { [key]: { value } })) + } + /> + ) : ( + + )} - {DocsLink} + ); }; -const GcpSetupAccessSelector = ({ onChange }: { onChange(type: GcpCredentialsType): void }) => ( - onChange(id)} - /> -); - const GcpInputVarFields = ({ fields, onChange, @@ -278,7 +439,7 @@ const GcpInputVarFields = ({ data-test-subj={CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.CREDENTIALS_TYPE} fullWidth options={credentialOptionsList} - value={credentialsTypeFields?.value} + value={credentialsTypeFields?.value || credentialOptionsList[0].value} onChange={(optionElem) => { onChange('credentials_type', optionElem.target.value); }} diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx index 808a3164fb41c..213d03b95266f 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx @@ -196,7 +196,7 @@ describe('', () => { it('renders CSPM input selector', () => { const { getByLabelText } = render(); - const option1 = getByLabelText('Amazon Web Services'); + const option1 = getByLabelText('AWS'); const option2 = getByLabelText('GCP'); const option3 = getByLabelText('Azure'); @@ -229,7 +229,7 @@ describe('', () => { ); - const option1 = getByLabelText('Amazon Web Services'); + const option1 = getByLabelText('AWS'); const option2 = getByLabelText('GCP'); const option3 = getByLabelText('Azure'); @@ -983,12 +983,12 @@ describe('', () => { let policy = getMockPolicyGCP(); policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { credentials_type: { value: 'credentials-file' }, + setup_access: { value: 'manual' }, }); const { getByText } = render( ); - expect(onChange).toHaveBeenCalledWith({ isValid: false, updatedPolicy: policy, @@ -1001,45 +1001,80 @@ describe('', () => { ).toBeInTheDocument(); }); - it(`renders ${CLOUDBEAT_GCP} Credentials File fields`, () => { + it(`renders Google Cloud Shell forms when Setup Access is set to Google Cloud Shell`, () => { let policy = getMockPolicyGCP(); policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { credentials_type: { value: 'credentials-file' }, + setup_access: { value: 'google_cloud_shell' }, }); - const { getByLabelText, getByRole } = render( + const { getByTestId } = render( ); - - expect(getByRole('option', { name: 'Credentials File', selected: true })).toBeInTheDocument(); + expect(onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: policy, + }); expect( - getByLabelText('Path to JSON file containing the credentials and key used to subscribe') + getByTestId(CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.GOOGLE_CLOUD_SHELL_SETUP) ).toBeInTheDocument(); }); - it(`updates ${CLOUDBEAT_GCP} Credentials File fields`, () => { + it(`project ID is required for Manual users`, () => { + let policy = getMockPolicyGCP(); + policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { + project_id: { value: undefined }, + setup_access: { value: 'manual' }, + }); + + const { rerender } = render( + + ); + expect(onChange).toHaveBeenCalledWith({ + isValid: false, + updatedPolicy: policy, + }); + policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { + project_id: { value: '' }, + setup_access: { value: 'manual' }, + }); + rerender(); + expect(onChange).toHaveBeenCalledWith({ + isValid: false, + updatedPolicy: policy, + }); + }); + + it(`renders ${CLOUDBEAT_GCP} Credentials File fields`, () => { let policy = getMockPolicyGCP(); policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { credentials_type: { value: 'credentials-file' }, + setup_access: { value: 'manual' }, }); - const { rerender, getByTestId } = render( + const { getByLabelText, getByRole } = render( ); - userEvent.type(getByTestId(CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.PROJECT_ID), 'a'); + expect(getByRole('option', { name: 'Credentials File', selected: true })).toBeInTheDocument(); + expect( + getByLabelText('Path to JSON file containing the credentials and key used to subscribe') + ).toBeInTheDocument(); + }); + + it(`updates ${CLOUDBEAT_GCP} Credentials File fields`, () => { + let policy = getMockPolicyGCP(); policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { project_id: { value: 'a' }, + credentials_type: { value: 'credentials-file' }, + setup_access: { value: 'manual' }, }); - expect(onChange).toHaveBeenCalledWith({ - isValid: true, - updatedPolicy: policy, - }); - - rerender(); + const { getByTestId } = render( + + ); userEvent.type(getByTestId(CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.CREDENTIALS_FILE), 'b'); @@ -1047,7 +1082,7 @@ describe('', () => { credentials_file: { value: 'b' }, }); - expect(onChange).toHaveBeenNthCalledWith(5, { + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: policy, }); @@ -1056,10 +1091,11 @@ describe('', () => { it(`renders ${CLOUDBEAT_GCP} Credentials JSON fields`, () => { let policy = getMockPolicyGCP(); policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { + setup_access: { value: 'manual' }, credentials_type: { value: 'credentials-json' }, }); - const { getByLabelText, getByRole } = render( + const { getByRole, getByLabelText } = render( ); @@ -1073,33 +1109,22 @@ describe('', () => { it(`updates ${CLOUDBEAT_GCP} Credentials JSON fields`, () => { let policy = getMockPolicyGCP(); policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { + project_id: { value: 'a' }, credentials_type: { value: 'credentials-json' }, + setup_access: { value: 'manual' }, }); - const { rerender, getByTestId } = render( + const { getByTestId } = render( ); - userEvent.type(getByTestId(CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.PROJECT_ID), 'a'); - - policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { - project_id: { value: 'a' }, - }); - - expect(onChange).toHaveBeenCalledWith({ - isValid: true, - updatedPolicy: policy, - }); - - rerender(); - userEvent.type(getByTestId(CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.CREDENTIALS_JSON), 'b'); policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { credentials_json: { value: 'b' }, }); - expect(onChange).toHaveBeenNthCalledWith(5, { + expect(onChange).toHaveBeenCalledWith({ isValid: true, updatedPolicy: policy, }); diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx index 5242de03a0058..e70e16f82762f 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx @@ -81,6 +81,8 @@ interface IntegrationInfoFieldsProps { export const AWS_SINGLE_ACCOUNT = 'single-account'; export const AWS_ORGANIZATION_ACCOUNT = 'organization-account'; +export const GCP_SINGLE_ACCOUNT = 'single-account-gcp'; +export const GCP_ORGANIZATION_ACCOUNT = 'organization-account-gcp'; type AwsAccountType = typeof AWS_SINGLE_ACCOUNT | typeof AWS_ORGANIZATION_ACCOUNT; const getAwsAccountTypeOptions = (isAwsOrgDisabled: boolean): CspRadioGroupProps['options'] => [ @@ -104,6 +106,28 @@ const getAwsAccountTypeOptions = (isAwsOrgDisabled: boolean): CspRadioGroupProps }, ]; +const getGcpAccountTypeOptions = (): CspRadioGroupProps['options'] => [ + { + id: GCP_ORGANIZATION_ACCOUNT, + label: i18n.translate('xpack.csp.fleetIntegration.gcpAccountType.gcpOrganizationLabel', { + defaultMessage: 'GCP Organization', + }), + disabled: true, + tooltip: i18n.translate( + 'xpack.csp.fleetIntegration.gcpAccountType.gcpOrganizationDisabledTooltip', + { + defaultMessage: 'Coming Soon', + } + ), + }, + { + id: GCP_SINGLE_ACCOUNT, + label: i18n.translate('xpack.csp.fleetIntegration.gcpAccountType.gcpSingleAccountLabel', { + defaultMessage: 'Single Account', + }), + }, +]; + const getAwsAccountType = ( input: Extract ): AwsAccountType | undefined => input.streams[0].vars?.['aws.account_type']?.value; @@ -208,6 +232,53 @@ const AwsAccountTypeSelect = ({ ); }; +const GcpAccountTypeSelect = ({ + input, + newPolicy, + updatePolicy, + packageInfo, +}: { + input: Extract; + newPolicy: NewPackagePolicy; + updatePolicy: (updatedPolicy: NewPackagePolicy) => void; + packageInfo: PackageInfo; +}) => { + return ( + <> + + + + + { + updatePolicy( + getPosturePolicy(newPolicy, input.type, { + gcp_account_type: { + value: accountType, + type: 'text', + }, + }) + ); + }} + size="m" + /> + + + + + + + ); +}; + const IntegrationSettings = ({ onChange, fields }: IntegrationInfoFieldsProps) => (
{fields.map(({ value, id, label, error }) => ( @@ -375,6 +446,15 @@ export const CspPolicyTemplateForm = memo )} + {input.type === 'cloudbeat/cis_gcp' && ( + + )} + {/* Defines the name/description */} { + if (!packageInfo.policy_templates) return ''; + + const policyTemplate = packageInfo.policy_templates.find((p) => p.name === CSPM_POLICY_TEMPLATE); + if (!policyTemplate) return ''; + + const policyTemplateInputs = hasPolicyTemplateInputs(policyTemplate) && policyTemplate.inputs; + + if (!policyTemplateInputs) return ''; + + const cloudShellUrl = policyTemplateInputs.reduce((acc, input): string => { + if (!input.vars) return acc; + const template = input.vars.find((v) => v.name === 'cloud_shell_url')?.default; + return template ? String(template) : acc; + }, ''); + + return cloudShellUrl; +}; diff --git a/x-pack/plugins/enterprise_search/common/connectors/connectors.ts b/x-pack/plugins/enterprise_search/common/connectors/connectors.ts index 1c5a3f30dcc4a..a1d5b87d9f965 100644 --- a/x-pack/plugins/enterprise_search/common/connectors/connectors.ts +++ b/x-pack/plugins/enterprise_search/common/connectors/connectors.ts @@ -150,6 +150,17 @@ export const CONNECTOR_DEFINITIONS: ConnectorServerSideDefinition[] = [ }), serviceType: 'dropbox', }, + { + iconPath: 'gmail.svg', + isBeta: false, + isNative: false, + isTechPreview: true, + keywords: ['google', 'gmail', 'connector', 'mail'], + name: i18n.translate('xpack.enterpriseSearch.content.nativeConnectors.gmail.name', { + defaultMessage: 'Gmail', + }), + serviceType: 'gmail', + }, { iconPath: 'oracle.svg', isBeta: true, @@ -181,6 +192,17 @@ export const CONNECTOR_DEFINITIONS: ConnectorServerSideDefinition[] = [ }), serviceType: 'servicenow', }, + { + iconPath: 'slack.svg', + isBeta: false, + isNative: false, + isTechPreview: true, + keywords: ['slack', 'connector'], + name: i18n.translate('xpack.enterpriseSearch.content.nativeConnectors.slack.name', { + defaultMessage: 'Slack', + }), + serviceType: 'slack', + }, { iconPath: 'sharepoint_server.svg', isBeta: true, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_section.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_section.tsx index 1bf181c44d708..4be1cfb5ef9f3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_section.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_section.tsx @@ -11,7 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, - EuiPageContentBody_Deprecated as EuiPageContentBody, + EuiPageSection, EuiSpacer, EuiText, EuiTitle, @@ -19,9 +19,9 @@ import { } from '@elastic/eui'; interface Props { - title: string; - subtitle: string; iconType?: IconType; + subtitle: string; + title: string; } export const AnalyticsSection: React.FC = ({ title, subtitle, iconType, children }) => (
@@ -49,6 +49,6 @@ export const AnalyticsSection: React.FC = ({ title, subtitle, iconType, c - {children} + {children}
); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx index 1cf22f472fc4c..9b85406225b76 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx @@ -16,8 +16,7 @@ import { EuiButtonIcon, EuiSpacer, EuiButton, - EuiPageContentHeader_Deprecated as EuiPageContentHeader, - EuiPageContentHeaderSection_Deprecated as EuiPageContentHeaderSection, + EuiPageHeader, EuiSkeletonText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -85,31 +84,27 @@ export const Credentials: React.FC = () => { - - - -

- {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.apiKeys', { - defaultMessage: 'API keys', - })} -

-
-
- - {!dataLoading && ( - showCredentialsForm()} - > - {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.createKey', { - defaultMessage: 'Create key', - })} - - )} - -
+ + +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.apiKeys', { + defaultMessage: 'API keys', + })} +

+
+ {!dataLoading && ( + showCredentialsForm()} + > + {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.createKey', { + defaultMessage: 'Create key', + })} + + )} +
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx index b9d571cc315f6..24ab03d5e93a7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx @@ -13,13 +13,13 @@ import { EuiSpacer, EuiPageHeader, EuiTitle, - EuiPageContentBody_Deprecated as EuiPageContentBody, - EuiPageContent_Deprecated as EuiPageContent, + EuiPageSection, EuiDragDropContext, EuiDroppable, EuiDraggable, EuiButtonIconProps, EuiEmptyPrompt, + EuiPageBody, } from '@elastic/eui'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; @@ -167,8 +167,8 @@ export const Library: React.FC = () => { <> - - + +

Result

@@ -540,8 +540,8 @@ export const Library: React.FC = () => { -
-
+ + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/constants.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/constants.ts index 24b9d342f52d4..bea446bf528c0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/constants.ts @@ -37,6 +37,12 @@ export const CONNECTORS_DICT: Record = { externalDocsUrl: '', icon: CONNECTOR_ICONS.dropbox, }, + gmail: { + docsUrl: '', // TODO + externalAuthDocsUrl: '', + externalDocsUrl: '', + icon: CONNECTOR_ICONS.gmail, + }, google_cloud_storage: { docsUrl: docLinks.connectorsGoogleCloudStorage, externalAuthDocsUrl: 'https://cloud.google.com/storage/docs/authentication', @@ -118,6 +124,13 @@ export const CONNECTORS_DICT: Record = { icon: CONNECTOR_ICONS.sharepoint_online, platinumOnly: true, }, + slack: { + docsUrl: '', // TODO + externalAuthDocsUrl: '', + externalDocsUrl: '', + icon: CONNECTOR_ICONS.slack, + platinumOnly: true, + }, }; export const CONNECTORS = CONNECTOR_DEFINITIONS.map((connector) => ({ diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/icons/connector_icons.ts b/x-pack/plugins/enterprise_search/public/applications/shared/icons/connector_icons.ts index 5533e6e5d3d3c..0f41254093984 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/icons/connector_icons.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/icons/connector_icons.ts @@ -10,6 +10,7 @@ import confluence_cloud from '../../../assets/source_icons/confluence_cloud.svg' import custom from '../../../assets/source_icons/custom.svg'; import dropbox from '../../../assets/source_icons/dropbox.svg'; import github from '../../../assets/source_icons/github.svg'; +import gmail from '../../../assets/source_icons/gmail.svg'; import google_cloud_storage from '../../../assets/source_icons/google_cloud_storage.svg'; import google_drive from '../../../assets/source_icons/google_drive.svg'; import jira_cloud from '../../../assets/source_icons/jira_cloud.svg'; @@ -23,6 +24,7 @@ import amazon_s3 from '../../../assets/source_icons/s3.svg'; import servicenow from '../../../assets/source_icons/servicenow.svg'; import sharepoint from '../../../assets/source_icons/sharepoint.svg'; import sharepoint_online from '../../../assets/source_icons/sharepoint_online.svg'; +import slack from '../../../assets/source_icons/slack.svg'; export const CONNECTOR_ICONS = { amazon_s3, @@ -31,6 +33,7 @@ export const CONNECTOR_ICONS = { custom, dropbox, github, + gmail, google_cloud_storage, google_drive, jira_cloud, @@ -43,4 +46,5 @@ export const CONNECTOR_ICONS = { servicenow, sharepoint, sharepoint_online, + slack, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx index 84c634cd8633d..d19e264b30df2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx @@ -9,13 +9,7 @@ import React from 'react'; -import { - EuiPageContent_Deprecated as EuiPageContent, - EuiSteps, - EuiText, - EuiLink, - EuiCallOut, -} from '@elastic/eui'; +import { EuiPageSection, EuiSteps, EuiText, EuiLink, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -27,7 +21,7 @@ interface Props { } export const CloudSetupInstructions: React.FC = ({ productName, cloudDeploymentLink }) => ( - + = ({ productName, cloudDepl }, ]} /> - + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/instructions.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/instructions.tsx index dc21bfd608840..6e0c75f1beb80 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/instructions.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/instructions.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { - EuiPageContent_Deprecated as EuiPageContent, + EuiPageSection, EuiText, EuiSteps, EuiCode, @@ -27,7 +27,7 @@ interface Props { } export const SetupInstructions: React.FC = ({ productName }) => ( - + = ({ productName }) => ( }, ]} /> - + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx index 9b3a3e61f70ad..7d277e3977ce2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx @@ -11,7 +11,7 @@ import { useValues } from 'kea'; import { EuiPage, - EuiPageSideBar_Deprecated as EuiPageSideBar, + EuiPageSidebar, EuiPageBody, EuiSpacer, EuiFlexGroup, @@ -35,8 +35,8 @@ import './setup_guide.scss'; interface Props { children: React.ReactNode; - productName: string; productEuiIcon: 'logoAppSearch' | 'logoWorkplaceSearch' | 'logoEnterpriseSearch'; + productName: string; } export const SetupGuideLayout: React.FC = ({ children, productName, productEuiIcon }) => { @@ -46,7 +46,7 @@ export const SetupGuideLayout: React.FC = ({ children, productName, produ return ( - + {SETUP_GUIDE_TITLE} @@ -64,7 +64,7 @@ export const SetupGuideLayout: React.FC = ({ children, productName, produ {children} - + {isCloudEnabled ? ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss index 3287cb21783cb..317c87c3516a2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss @@ -18,6 +18,7 @@ } &__body { + padding-top:0; position: relative; width: 100%; height: 100%; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx index 5ea3783fc206f..1b87f8fdce4b2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx @@ -11,12 +11,12 @@ import { useRouteMatch } from 'react-router-dom'; import { useValues } from 'kea'; import { - EuiPage, - EuiPageSideBar_Deprecated as EuiPageSideBar, + EuiPageSidebar, EuiPageBody, - EuiPageContentBody_Deprecated as EuiPageContentBody, + EuiPageSection, EuiCallOut, EuiSpacer, + EuiPageTemplate, } from '@elastic/eui'; import { AccountHeader, AccountSettingsSidebar, PrivateSourcesSidebar } from '..'; @@ -47,13 +47,22 @@ export const PersonalDashboardLayout: React.FC = ({ <> {pageChrome && } - - + + {useRouteMatch(PRIVATE_SOURCES_PATH) && } {useRouteMatch(PERSONAL_SETTINGS_PATH) && } - + - + {readOnlyMode && ( <> = ({ )} + {isLoading ? : children} - + - + ); }; diff --git a/x-pack/plugins/enterprise_search/server/integrations.ts b/x-pack/plugins/enterprise_search/server/integrations.ts index 8e60b6b73ff40..6d2e67d602548 100644 --- a/x-pack/plugins/enterprise_search/server/integrations.ts +++ b/x-pack/plugins/enterprise_search/server/integrations.ts @@ -34,19 +34,6 @@ const workplaceSearchIntegrations: WorkplaceSearchIntegration[] = [ ), categories: ['enterprise_search', 'workplace_search_content_source'], }, - { - id: 'gmail', - title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.gmailName', { - defaultMessage: 'Gmail', - }), - description: i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.integrations.gmailDescription', - { - defaultMessage: 'Search over your emails managed by Gmail with Workplace Search.', - } - ), - categories: ['enterprise_search', 'google_cloud', 'workplace_search_content_source'], - }, { id: 'onedrive', title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.onedriveName', { @@ -77,19 +64,6 @@ const workplaceSearchIntegrations: WorkplaceSearchIntegration[] = [ ), categories: ['enterprise_search', 'workplace_search_content_source'], }, - { - id: 'slack', - title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.slackName', { - defaultMessage: 'Slack', - }), - description: i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.integrations.slackDescription', - { - defaultMessage: 'Search over your messages on Slack with Workplace Search.', - } - ), - categories: ['enterprise_search', 'workplace_search_content_source'], - }, { id: 'zendesk', title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.zendeskName', { @@ -303,6 +277,27 @@ export const registerEnterpriseSearchIntegrations = ( isBeta: false, }); + customIntegrations.registerCustomIntegration({ + id: 'gmail', + title: i18n.translate('xpack.enterpriseSearch.content.integrations.gmail', { + defaultMessage: 'Gmail', + }), + description: i18n.translate('xpack.enterpriseSearch.content.integrations.gmailDescription', { + defaultMessage: 'Search over your content on Gmail.', + }), + categories: ['enterprise_search', 'elastic_stack', 'connector', 'connector_client'], + uiInternalPath: + '/app/enterprise_search/content/search_indices/new_index/connector?service_type=gmail', + icons: [ + { + type: 'svg', + src: http.basePath.prepend('/plugins/enterpriseSearch/assets/source_icons/gmail.svg'), + }, + ], + shipper: 'enterprise_search', + isBeta: false, + }); + customIntegrations.registerCustomIntegration({ id: 'mongodb', title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.mongoDBName', { @@ -547,6 +542,27 @@ export const registerEnterpriseSearchIntegrations = ( isBeta: false, }); + customIntegrations.registerCustomIntegration({ + id: 'slack', + title: i18n.translate('xpack.enterpriseSearch.content.integrations.slack', { + defaultMessage: 'Slack', + }), + description: i18n.translate('xpack.enterpriseSearch.content.integrations.slackDescription', { + defaultMessage: 'Search over your content on Slack.', + }), + categories: ['enterprise_search', 'elastic_stack', 'connector', 'connector_client'], + uiInternalPath: + '/app/enterprise_search/content/search_indices/new_index/connector?service_type=slack', + icons: [ + { + type: 'svg', + src: http.basePath.prepend('/plugins/enterpriseSearch/assets/source_icons/slack.svg'), + }, + ], + shipper: 'enterprise_search', + isBeta: false, + }); + customIntegrations.registerCustomIntegration({ id: 'oracle', title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.oracleName', { diff --git a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx index 959bf0a68b3e1..f8e86388131aa 100644 --- a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx +++ b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx @@ -10,8 +10,8 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { LensEmbeddableInput, TypedLensByValueInput } from '@kbn/lens-plugin/public'; import { - useObservabilityAIAssistant, ObservabilityAIAssistantActionMenuItem, + useObservabilityAIAssistantOptional, } from '@kbn/observability-ai-assistant-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { EmbedAction } from '../../header/embed_action'; @@ -33,7 +33,7 @@ export function ExpViewActionMenuContent({ const LensSaveModalComponent = lens.SaveModalComponent; - const service = useObservabilityAIAssistant(); + const service = useObservabilityAIAssistantOptional(); return ( <> @@ -99,7 +99,7 @@ export function ExpViewActionMenuContent({ })} - {service.isEnabled() ? ( + {service?.isEnabled() ? ( diff --git a/x-pack/plugins/fleet/cypress/screens/fleet.ts b/x-pack/plugins/fleet/cypress/screens/fleet.ts index e6b6ac1f47008..3b8ffcc63b6f6 100644 --- a/x-pack/plugins/fleet/cypress/screens/fleet.ts +++ b/x-pack/plugins/fleet/cypress/screens/fleet.ts @@ -121,6 +121,8 @@ export const SETTINGS_OUTPUTS = { NAME_INPUT: 'settingsOutputsFlyout.nameInput', TYPE_INPUT: 'settingsOutputsFlyout.typeInput', ADD_HOST_ROW_BTN: 'fleetServerHosts.multiRowInput.addRowButton', + WARNING_KAFKA_CALLOUT: 'settingsOutputsFlyout.kafkaOutputTypeCallout', + WARNING_ELASTICSEARCH_CALLOUT: 'settingsOutputsFlyout.elasticsearchOutputTypeCallout', }; export const getSpecificSelectorId = (selector: string, id: number) => { diff --git a/x-pack/plugins/fleet/cypress/screens/fleet_outputs.ts b/x-pack/plugins/fleet/cypress/screens/fleet_outputs.ts index de6ef1097b74a..0e018cd301d1b 100644 --- a/x-pack/plugins/fleet/cypress/screens/fleet_outputs.ts +++ b/x-pack/plugins/fleet/cypress/screens/fleet_outputs.ts @@ -21,6 +21,7 @@ export const selectKafkaOutput = () => { visit('/app/fleet/settings'); cy.getBySel(SETTINGS_OUTPUTS.ADD_BTN).click(); cy.getBySel(SETTINGS_OUTPUTS.TYPE_INPUT).select('kafka'); + cy.getBySel(SETTINGS_OUTPUTS.WARNING_KAFKA_CALLOUT); cy.getBySel(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_USERNAME_PASSWORD_OPTION).click(); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/utils/install_command_utils.ts b/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/utils/install_command_utils.ts index 0221d332692b9..b528c5092ea14 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/utils/install_command_utils.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/utils/install_command_utils.ts @@ -52,6 +52,9 @@ function getArtifact(platform: PLATFORM_TYPE, kibanaVersion: string) { kubernetes: { downloadCommand: '', }, + googleCloudShell: { + downloadCommand: '', + }, }; return artifactMap[platform]; @@ -116,6 +119,7 @@ export function getInstallCommandForPlatform( rpm: `${artifact.downloadCommand}\nsudo elastic-agent enroll ${commandArgumentsStr}\nsudo systemctl enable elastic-agent\nsudo systemctl start elastic-agent`, kubernetes: '', cloudFormation: '', + googleCloudShell: '', }; return commands[platform]; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_google_cloud_shell_modal.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_google_cloud_shell_modal.tsx new file mode 100644 index 0000000000000..3879f44d5fbe0 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_google_cloud_shell_modal.tsx @@ -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 React from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useQuery } from '@tanstack/react-query'; + +import type { AgentPolicy, PackagePolicy } from '../../../../../types'; +import { + sendGetEnrollmentAPIKeys, + useCreateCloudShellUrl, + useFleetServerHostsForPolicy, + useKibanaVersion, +} from '../../../../../hooks'; +import { GoogleCloudShellGuide } from '../../../../../components'; +import { ManualInstructions } from '../../../../../../../components/enrollment_instructions'; + +export const PostInstallGoogleCloudShellModal: React.FunctionComponent<{ + onConfirm: () => void; + onCancel: () => void; + agentPolicy: AgentPolicy; + packagePolicy: PackagePolicy; +}> = ({ onConfirm, onCancel, agentPolicy, packagePolicy }) => { + const { data: apyKeysData } = useQuery(['googleCloudShellApiKeys'], () => + sendGetEnrollmentAPIKeys({ + page: 1, + perPage: 1, + kuery: `policy_id:${agentPolicy.id}`, + }) + ); + const { fleetServerHosts, fleetProxy } = useFleetServerHostsForPolicy(agentPolicy); + const kibanaVersion = useKibanaVersion(); + + const installManagedCommands = ManualInstructions({ + apiKey: apyKeysData?.data?.items[0]?.api_key || 'no_key', + fleetServerHosts, + fleetProxy, + kibanaVersion, + }); + + const { cloudShellUrl, error, isError, isLoading } = useCreateCloudShellUrl({ + enrollmentAPIKey: apyKeysData?.data?.items[0]?.api_key, + packagePolicy, + }); + + return ( + + + + + + + + + + {error && isError && ( + <> + + + + )} + + + + + + + { + window.open(cloudShellUrl); + onConfirm(); + }} + fill + color="primary" + isLoading={isLoading} + isDisabled={isError} + > + + + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx index dbb901316cece..da4ecfbe3569a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx @@ -24,7 +24,11 @@ import { sendBulkInstallPackages, sendGetPackagePolicies, } from '../../../../../hooks'; -import { isVerificationError, packageToPackagePolicy } from '../../../../../services'; +import { + getCloudShellUrlFromPackagePolicy, + isVerificationError, + packageToPackagePolicy, +} from '../../../../../services'; import { FLEET_ELASTIC_AGENT_PACKAGE, FLEET_SYSTEM_PACKAGE, @@ -304,11 +308,18 @@ export function useOnSubmit({ ? getCloudFormationPropsFromPackagePolicy(data.item).templateUrl : false; + const hasGoogleCloudShell = data?.item ? getCloudShellUrlFromPackagePolicy(data.item) : false; + if (hasCloudFormation) { setFormState(agentCount ? 'SUBMITTED' : 'SUBMITTED_CLOUD_FORMATION'); } else { setFormState(agentCount ? 'SUBMITTED' : 'SUBMITTED_NO_AGENTS'); } + if (hasGoogleCloudShell) { + setFormState(agentCount ? 'SUBMITTED' : 'SUBMITTED_GOOGLE_CLOUD_SHELL'); + } else { + setFormState(agentCount ? 'SUBMITTED' : 'SUBMITTED_NO_AGENTS'); + } if (!error) { setSavedPackagePolicy(data!.item); @@ -317,6 +328,10 @@ export function useOnSubmit({ setFormState('SUBMITTED_CLOUD_FORMATION'); return; } + if (!hasAgentsAssigned && hasGoogleCloudShell) { + setFormState('SUBMITTED_GOOGLE_CLOUD_SHELL'); + return; + } if (!hasAgentsAssigned) { setFormState('SUBMITTED_NO_AGENTS'); return; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx index 9b26311699ea7..e7a35ae48dbda 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx @@ -60,6 +60,7 @@ import { generateNewAgentPolicyWithDefaults } from '../../../../../../../common/ import { CreatePackagePolicySinglePageLayout, PostInstallAddAgentModal } from './components'; import { useDevToolsRequest, useOnSubmit } from './hooks'; import { PostInstallCloudFormationModal } from './components/post_install_cloud_formation_modal'; +import { PostInstallGoogleCloudShellModal } from './components/post_install_google_cloud_shell_modal'; const StepsWithLessPadding = styled(EuiSteps)` .euiStep__content { @@ -422,6 +423,14 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ onCancel={() => navigateAddAgentHelp(savedPackagePolicy)} /> )} + {formState === 'SUBMITTED_GOOGLE_CLOUD_SHELL' && agentPolicy && savedPackagePolicy && ( + navigateAddAgent(savedPackagePolicy)} + onCancel={() => navigateAddAgentHelp(savedPackagePolicy)} + /> + )} {packageInfo && ( = [proxies] ); - const isESOutput = inputs.typeInput.value === outputType.Elasticsearch; const { kafkaOutput: isKafkaOutputEnabled } = ExperimentalFeaturesService.get(); const OUTPUT_TYPE_OPTIONS = [ @@ -249,6 +248,43 @@ export const EditOutputFlyout: React.FunctionComponent = } }; + const renderTypeSpecificWarning = () => { + const isESOutput = inputs.typeInput.value === outputType.Elasticsearch; + const isKafkaOutput = inputs.typeInput.value === outputType.Kafka; + if (!isKafkaOutput && !isESOutput) { + return null; + } + + const generateWarningMessage = () => { + switch (inputs.typeInput.value) { + case outputType.Kafka: + return i18n.translate('xpack.fleet.settings.editOutputFlyout.kafkaOutputTypeCallout', { + defaultMessage: + 'Kafka output is currently not supported on Agents using the Elastic Defend integration.', + }); + default: + case outputType.Elasticsearch: + return i18n.translate('xpack.fleet.settings.editOutputFlyout.esOutputTypeCallout', { + defaultMessage: + 'This output type currently does not support connectivity to a remote Elasticsearch cluster.', + }); + } + }; + return ( + <> + + + + ); + }; + return ( @@ -350,24 +386,7 @@ export const EditOutputFlyout: React.FunctionComponent = } )} /> - {isESOutput && ( - <> - - - - )} + {renderTypeSpecificWarning()} diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/google_cloud_shell_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/google_cloud_shell_instructions.tsx new file mode 100644 index 0000000000000..89b6f67fe0dcf --- /dev/null +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/google_cloud_shell_instructions.tsx @@ -0,0 +1,43 @@ +/* + * 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 { EuiButton, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { GoogleCloudShellGuide } from '../google_cloud_shell_guide'; + +interface Props { + cloudShellUrl: string; + cloudShellCommand: string; +} + +export const GoogleCloudShellInstructions: React.FunctionComponent = ({ + cloudShellUrl, + cloudShellCommand, +}) => { + return ( + <> + + + + + + + ); +}; diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/hooks.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/hooks.tsx index f738d99533cf0..9345e4e3a6663 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/hooks.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/hooks.tsx @@ -14,6 +14,7 @@ import { FLEET_CLOUD_SECURITY_POSTURE_PACKAGE, FLEET_CLOUD_DEFEND_PACKAGE, } from '../../../common'; +import { getCloudShellUrlFromAgentPolicy } from '../../services'; import { getCloudFormationTemplateUrlFromPackageInfo, @@ -127,6 +128,7 @@ export function useCloudSecurityIntegration(agentPolicy?: AgentPolicy) { AWS_ACCOUNT_TYPE ]?.value; + const cloudShellUrl = getCloudShellUrlFromAgentPolicy(agentPolicy); return { isLoading, integrationType, @@ -135,6 +137,7 @@ export function useCloudSecurityIntegration(agentPolicy?: AgentPolicy) { awsAccountType: cloudFormationAwsAccountType, templateUrl: cloudFormationTemplateUrl, }, + cloudShellUrl, }; }, [agentPolicy, packageInfoData?.item, isLoading, cloudSecurityPackagePolicy]); diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/instructions.tsx index d88c9cedb4bcd..0a413856e4f34 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/instructions.tsx @@ -81,7 +81,10 @@ export const Instructions = (props: InstructionProps) => { useEffect(() => { // If we detect a CloudFormation integration, we want to hide the selection type - if (props.cloudSecurityIntegration?.isCloudFormation) { + if ( + props.cloudSecurityIntegration?.isCloudFormation || + props.cloudSecurityIntegration?.cloudShellUrl + ) { setSelectionType(undefined); } else if (!isIntegrationFlow && showAgentEnrollment) { setSelectionType('radio'); @@ -103,7 +106,7 @@ export const Instructions = (props: InstructionProps) => { } else if (showAgentEnrollment) { return ( <> - {selectionType === 'tabs' && ( + {selectionType === 'tabs' && !props.cloudSecurityIntegration?.cloudShellUrl && ( <> = ({ windowsCommand={installCommand.windows} linuxDebCommand={installCommand.deb} linuxRpmCommand={installCommand.rpm} + googleCloudShellCommand={installCommand.googleCloudShell} k8sCommand={installCommand.kubernetes} hasK8sIntegration={isK8s === 'IS_KUBERNETES' || isK8s === 'IS_KUBERNETES_MULTIPAGE'} cloudSecurityIntegration={cloudSecurityIntegration} diff --git a/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx index ff94307792f75..ce3015dd2ccbd 100644 --- a/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx +++ b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx @@ -35,6 +35,8 @@ export const ManualInstructions = ({ kibanaVersion: string; }) => { const enrollArgs = getfleetServerHostsEnrollArgs(apiKey, fleetServerHosts, fleetProxy); + const fleetServerUrl = enrollArgs?.split('--url=')?.pop()?.split('--enrollment')[0]; + const enrollmentToken = enrollArgs?.split('--enrollment-token=')[1]; const k8sCommand = 'kubectl apply -f elastic-agent-managed-kubernetes.yml'; @@ -62,6 +64,8 @@ sudo elastic-agent enroll ${enrollArgs} \nsudo systemctl enable elastic-agent \n sudo rpm -vi elastic-agent-${kibanaVersion}-x86_64.rpm sudo elastic-agent enroll ${enrollArgs} \nsudo systemctl enable elastic-agent \nsudo systemctl start elastic-agent`; + const googleCloudShellCommand = `FLEET_URL=${fleetServerUrl} ENROLLMENT_TOKEN=${enrollmentToken} STACK_VERSION=${kibanaVersion} ./deploy.sh`; + return { linux: linuxCommand, mac: macCommand, @@ -70,5 +74,6 @@ sudo elastic-agent enroll ${enrollArgs} \nsudo systemctl enable elastic-agent \n rpm: linuxRpmCommand, kubernetes: k8sCommand, cloudFormation: '', + googleCloudShell: googleCloudShellCommand, }; }; diff --git a/x-pack/plugins/fleet/public/components/enrollment_instructions/standalone/index.tsx b/x-pack/plugins/fleet/public/components/enrollment_instructions/standalone/index.tsx index 6994cf2a7ebc2..ca2754daf1b71 100644 --- a/x-pack/plugins/fleet/public/components/enrollment_instructions/standalone/index.tsx +++ b/x-pack/plugins/fleet/public/components/enrollment_instructions/standalone/index.tsx @@ -38,5 +38,6 @@ cd elastic-agent-${kibanaVersion}-windows-x86_64 deb: linuxDebCommand, rpm: linuxRpmCommand, kubernetes: k8sCommand, + googleCloudShell: '', }; }; diff --git a/x-pack/plugins/fleet/public/components/google_cloud_shell_guide.tsx b/x-pack/plugins/fleet/public/components/google_cloud_shell_guide.tsx new file mode 100644 index 0000000000000..0efe5719e3166 --- /dev/null +++ b/x-pack/plugins/fleet/public/components/google_cloud_shell_guide.tsx @@ -0,0 +1,78 @@ +/* + * 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 { EuiCodeBlock, EuiLink, EuiText, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +/* Need to change to the real URL */ +const GOOGLE_CLOUD_SHELL_EXTERNAL_DOC_URL = 'https://cloud.google.com/shell/docs'; + +const Link = ({ children, url }: { children: React.ReactNode; url: string }) => ( + + {children} + +); + +export const GoogleCloudShellGuide = (props: { commandText: string }) => { + return ( + <> + + +

+ + + + ), + }} + /> +

+ +
    +
  1. + +
  2. +
  3. + <> + + + + {props.commandText} + + +
  4. +
  5. + +
  6. +
+
+
+ + ); +}; diff --git a/x-pack/plugins/fleet/public/components/index.ts b/x-pack/plugins/fleet/public/components/index.ts index 8335f9fcfc61f..f578a11ab7c5a 100644 --- a/x-pack/plugins/fleet/public/components/index.ts +++ b/x-pack/plugins/fleet/public/components/index.ts @@ -30,3 +30,4 @@ export { HeaderReleaseBadge, InlineReleaseBadge } from './release_badge'; export { WithGuidedOnboardingTour } from './with_guided_onboarding_tour'; export { UninstallCommandFlyout } from './uninstall_command_flyout'; export { CloudFormationGuide } from './cloud_formation_guide'; +export { GoogleCloudShellGuide } from './google_cloud_shell_guide'; diff --git a/x-pack/plugins/fleet/public/components/platform_selector.tsx b/x-pack/plugins/fleet/public/components/platform_selector.tsx index eb8b9d898855e..a4f08c265a565 100644 --- a/x-pack/plugins/fleet/public/components/platform_selector.tsx +++ b/x-pack/plugins/fleet/public/components/platform_selector.tsx @@ -24,9 +24,15 @@ import { FLEET_CLOUD_SECURITY_POSTURE_CSPM_POLICY_TEMPLATE, } from '../../common/constants/epm'; import { type PLATFORM_TYPE } from '../hooks'; -import { REDUCED_PLATFORM_OPTIONS, PLATFORM_OPTIONS, usePlatform } from '../hooks'; +import { + REDUCED_PLATFORM_OPTIONS, + PLATFORM_OPTIONS, + PLATFORM_OPTIONS_CLOUD_SHELL, + usePlatform, +} from '../hooks'; import { KubernetesInstructions } from './agent_enrollment_flyout/kubernetes_instructions'; +import { GoogleCloudShellInstructions } from './agent_enrollment_flyout/google_cloud_shell_instructions'; import type { CloudSecurityIntegration } from './agent_enrollment_flyout/types'; interface Props { @@ -36,6 +42,7 @@ interface Props { linuxDebCommand: string; linuxRpmCommand: string; k8sCommand: string; + googleCloudShellCommand?: string | undefined; hasK8sIntegration: boolean; cloudSecurityIntegration?: CloudSecurityIntegration | undefined; hasK8sIntegrationMultiPage: boolean; @@ -58,6 +65,7 @@ export const PlatformSelector: React.FunctionComponent = ({ linuxDebCommand, linuxRpmCommand, k8sCommand, + googleCloudShellCommand, hasK8sIntegration, cloudSecurityIntegration, hasK8sIntegrationMultiPage, @@ -68,6 +76,9 @@ export const PlatformSelector: React.FunctionComponent = ({ onCopy, }) => { const getInitialPlatform = useCallback(() => { + if (cloudSecurityIntegration?.cloudShellUrl) { + return 'googleCloudShell'; + } if ( hasK8sIntegration || (cloudSecurityIntegration?.integrationType === @@ -77,19 +88,28 @@ export const PlatformSelector: React.FunctionComponent = ({ return 'kubernetes'; return 'linux'; - }, [hasK8sIntegration, cloudSecurityIntegration?.integrationType, isManaged]); + }, [ + hasK8sIntegration, + cloudSecurityIntegration?.integrationType, + isManaged, + cloudSecurityIntegration?.cloudShellUrl, + ]); const { platform, setPlatform } = usePlatform(getInitialPlatform()); // In case of fleet server installation or standalone agent without // Kubernetes integration in the policy use reduced platform options + // If it has Cloud Shell URL, then it should show platform options with Cloudshell in it const isReduced = hasFleetServer || (!isManaged && !hasK8sIntegration); const getPlatformOptions = useCallback(() => { const platformOptions = isReduced ? REDUCED_PLATFORM_OPTIONS : PLATFORM_OPTIONS; + const platformOptionsWithCloudShell = cloudSecurityIntegration?.cloudShellUrl + ? PLATFORM_OPTIONS_CLOUD_SHELL + : platformOptions; - return platformOptions; - }, [isReduced]); + return platformOptionsWithCloudShell; + }, [isReduced, cloudSecurityIntegration?.cloudShellUrl]); const [copyButtonClicked, setCopyButtonClicked] = useState(false); @@ -144,6 +164,7 @@ export const PlatformSelector: React.FunctionComponent = ({ deb: linuxDebCommand, rpm: linuxRpmCommand, kubernetes: k8sCommand, + googleCloudShell: k8sCommand, }; const onTextAreaClick = () => { if (onCopy) onCopy(); @@ -208,6 +229,15 @@ export const PlatformSelector: React.FunctionComponent = ({ )} + {platform === 'googleCloudShell' && isManaged && ( + <> + + + + )} {!hasK8sIntegrationMultiPage && ( <> {platform === 'kubernetes' && ( @@ -220,17 +250,20 @@ export const PlatformSelector: React.FunctionComponent = ({
)} - - {commandsByPlatform[platform]} - + {platform !== 'googleCloudShell' && ( + + {commandsByPlatform[platform]} + + )} + {fullCopyButton && ( diff --git a/x-pack/plugins/fleet/public/hooks/index.ts b/x-pack/plugins/fleet/public/hooks/index.ts index 0692ce961379e..eaddfbaa08009 100644 --- a/x-pack/plugins/fleet/public/hooks/index.ts +++ b/x-pack/plugins/fleet/public/hooks/index.ts @@ -33,3 +33,4 @@ export * from './use_fleet_server_hosts_for_policy'; export * from './use_fleet_server_standalone'; export * from './use_locator'; export * from './use_create_cloud_formation_url'; +export * from './use_create_cloud_shell_url'; diff --git a/x-pack/plugins/fleet/public/hooks/use_create_cloud_shell_url.ts b/x-pack/plugins/fleet/public/hooks/use_create_cloud_shell_url.ts new file mode 100644 index 0000000000000..d8d26d45846b0 --- /dev/null +++ b/x-pack/plugins/fleet/public/hooks/use_create_cloud_shell_url.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 { i18n } from '@kbn/i18n'; + +import type { PackagePolicy } from '../../common'; +import { getCloudShellUrlFromPackagePolicy } from '../services'; + +import { useGetSettings } from './use_request'; + +export const useCreateCloudShellUrl = ({ + enrollmentAPIKey, + packagePolicy, +}: { + enrollmentAPIKey: string | undefined; + packagePolicy?: PackagePolicy; +}) => { + const { data, isLoading } = useGetSettings(); + + let isError = false; + let error: string | undefined; + + // Default fleet server host + const fleetServerHost = data?.item.fleet_server_hosts?.[0]; + + if (!fleetServerHost && !isLoading) { + isError = true; + error = i18n.translate('xpack.fleet.agentEnrollment.cloudShell.noFleetServerHost', { + defaultMessage: 'No Fleet Server host found', + }); + } + + if (!enrollmentAPIKey && !isLoading) { + isError = true; + error = i18n.translate('xpack.fleet.agentEnrollment.cloudShell.noApiKey', { + defaultMessage: 'No enrollment token found', + }); + } + + const cloudShellUrl = getCloudShellUrlFromPackagePolicy(packagePolicy) || ''; + + return { + isLoading, + cloudShellUrl, + isError, + error, + }; +}; diff --git a/x-pack/plugins/fleet/public/hooks/use_platform.tsx b/x-pack/plugins/fleet/public/hooks/use_platform.tsx index cc35477fbef12..7a5a4aae323b3 100644 --- a/x-pack/plugins/fleet/public/hooks/use_platform.tsx +++ b/x-pack/plugins/fleet/public/hooks/use_platform.tsx @@ -8,7 +8,14 @@ import { useState } from 'react'; import { i18n } from '@kbn/i18n'; -export type PLATFORM_TYPE = 'linux' | 'mac' | 'windows' | 'rpm' | 'deb' | 'kubernetes'; +export type PLATFORM_TYPE = + | 'linux' + | 'mac' + | 'windows' + | 'rpm' + | 'deb' + | 'kubernetes' + | 'googleCloudShell'; export const REDUCED_PLATFORM_OPTIONS: Array<{ label: string; @@ -63,6 +70,17 @@ export const PLATFORM_OPTIONS = [ }, ]; +export const PLATFORM_OPTIONS_CLOUD_SHELL = [ + ...PLATFORM_OPTIONS, + { + id: 'googleCloudShell', + label: i18n.translate('xpack.fleet.enrollmentInstructions.platformButtons.googleCloudShell', { + defaultMessage: 'Google Cloud Shell Script', + }), + 'data-test-subj': 'platformTypeGoogleCloudShellScript', + }, +]; + export function usePlatform(initialPlatform: PLATFORM_TYPE = 'linux') { const [platform, setPlatform] = useState(initialPlatform); diff --git a/x-pack/plugins/fleet/public/services/get_cloud_shell_url_from_agent_policy.test.ts b/x-pack/plugins/fleet/public/services/get_cloud_shell_url_from_agent_policy.test.ts new file mode 100644 index 0000000000000..ad7711917bd4a --- /dev/null +++ b/x-pack/plugins/fleet/public/services/get_cloud_shell_url_from_agent_policy.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { getCloudShellUrlFromAgentPolicy } from './get_cloud_shell_url_from_agent_policy'; + +describe('getCloudShellUrlFromAgentPolicy', () => { + it('should return undefined when selectedPolicy is undefined', () => { + const result = getCloudShellUrlFromAgentPolicy(); + expect(result).toBeUndefined(); + }); + + it('should return undefined when selectedPolicy has no package_policies', () => { + const selectedPolicy = {}; + // @ts-expect-error + const result = getCloudShellUrlFromAgentPolicy(selectedPolicy); + expect(result).toBeUndefined(); + }); + + it('should return undefined when no input has enabled and config.cloud_shell_url', () => { + const selectedPolicy = { + package_policies: [ + { + inputs: [ + { enabled: false, config: {} }, + { enabled: true, config: {} }, + { enabled: true, config: { other_property: 'value' } }, + ], + }, + { + inputs: [ + { enabled: false, config: {} }, + { enabled: false, config: {} }, + ], + }, + ], + }; + // @ts-expect-error + const result = getCloudShellUrlFromAgentPolicy(selectedPolicy); + expect(result).toBeUndefined(); + }); + + it('should return the first config.cloud_shell_url when available', () => { + const selectedPolicy = { + package_policies: [ + { + inputs: [ + { enabled: false, config: { cloud_shell_url: { value: 'url1' } } }, + { enabled: false, config: { cloud_shell_url: { value: 'url2' } } }, + { enabled: false, config: { other_property: 'value' } }, + ], + }, + { + inputs: [ + { enabled: false, config: {} }, + { enabled: true, config: { cloud_shell_url: { value: 'url3' } } }, + { enabled: true, config: { cloud_shell_url: { value: 'url4' } } }, + ], + }, + ], + }; + // @ts-expect-error + const result = getCloudShellUrlFromAgentPolicy(selectedPolicy); + expect(result).toBe('url3'); + }); +}); diff --git a/x-pack/plugins/fleet/public/services/get_cloud_shell_url_from_agent_policy.ts b/x-pack/plugins/fleet/public/services/get_cloud_shell_url_from_agent_policy.ts new file mode 100644 index 0000000000000..b41bfa9981859 --- /dev/null +++ b/x-pack/plugins/fleet/public/services/get_cloud_shell_url_from_agent_policy.ts @@ -0,0 +1,32 @@ +/* + * 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 type { AgentPolicy } from '../types'; + +/** + * Get the cloud shell url from a agent policy + * It looks for a config with a cloud_shell_url object present in + * the enabled package_policies inputs of the agent policy + */ +export const getCloudShellUrlFromAgentPolicy = (selectedPolicy?: AgentPolicy) => { + const cloudShellUrl = selectedPolicy?.package_policies?.reduce((acc, packagePolicy) => { + const findCloudShellUrlConfig = packagePolicy.inputs?.reduce((accInput, input) => { + if (accInput !== '') { + return accInput; + } + if (input?.enabled && input?.config?.cloud_shell_url) { + return input.config.cloud_shell_url.value; + } + return accInput; + }, ''); + if (findCloudShellUrlConfig) { + return findCloudShellUrlConfig; + } + return acc; + }, ''); + return cloudShellUrl !== '' ? cloudShellUrl : undefined; +}; diff --git a/x-pack/plugins/fleet/public/services/get_cloud_shell_url_from_package_policy.test.ts b/x-pack/plugins/fleet/public/services/get_cloud_shell_url_from_package_policy.test.ts new file mode 100644 index 0000000000000..389a481e98f8a --- /dev/null +++ b/x-pack/plugins/fleet/public/services/get_cloud_shell_url_from_package_policy.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 { getCloudShellUrlFromPackagePolicy } from './get_cloud_shell_url_from_package_policy'; + +describe('getCloudShellUrlFromPackagePolicyy', () => { + test('returns undefined when packagePolicy is undefined', () => { + const result = getCloudShellUrlFromPackagePolicy(undefined); + expect(result).toBeUndefined(); + }); + + test('returns undefined when packagePolicy is defined but inputs are empty', () => { + const packagePolicy = { inputs: [] }; + // @ts-expect-error + const result = getCloudShellUrlFromPackagePolicy(packagePolicy); + expect(result).toBeUndefined(); + }); + + test('returns undefined when no enabled input has a CloudShellUrl', () => { + const packagePolicy = { + inputs: [ + { enabled: false, config: { cloud_shell_url: { value: 'url1' } } }, + { enabled: false, config: { cloud_shell_url: { value: 'url2' } } }, + ], + }; + // @ts-expect-error + const result = getCloudShellUrlFromPackagePolicy(packagePolicy); + expect(result).toBeUndefined(); + }); + + test('returns the CloudShellUrl of the first enabled input', () => { + const packagePolicy = { + inputs: [ + { enabled: false, config: { cloud_shell_url: { value: 'url1' } } }, + { enabled: true, config: { cloud_shell_url: { value: 'url2' } } }, + { enabled: true, config: { cloud_shell_url: { value: 'url3' } } }, + ], + }; + // @ts-expect-error + const result = getCloudShellUrlFromPackagePolicy(packagePolicy); + expect(result).toBe('url2'); + }); + + test('returns the CloudShellUrl of the first enabled input and ignores subsequent inputs', () => { + const packagePolicy = { + inputs: [ + { enabled: true, config: { cloud_shell_url: { value: 'url1' } } }, + { enabled: true, config: { cloud_shell_url: { value: 'url2' } } }, + { enabled: true, config: { cloud_shell_url: { value: 'url3' } } }, + ], + }; + // @ts-expect-error + const result = getCloudShellUrlFromPackagePolicy(packagePolicy); + expect(result).toBe('url1'); + }); + + // Add more test cases as needed +}); diff --git a/x-pack/plugins/fleet/public/services/get_cloud_shell_url_from_package_policy.tsx b/x-pack/plugins/fleet/public/services/get_cloud_shell_url_from_package_policy.tsx new file mode 100644 index 0000000000000..158ead0b39ce3 --- /dev/null +++ b/x-pack/plugins/fleet/public/services/get_cloud_shell_url_from_package_policy.tsx @@ -0,0 +1,27 @@ +/* + * 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 type { PackagePolicy } from '../types'; + +/** + * Get the cloud shell url from a package policy + * It looks for a config with a cloud_shell_url object present in + * the enabled inputs of the package policy + */ +export const getCloudShellUrlFromPackagePolicy = (packagePolicy?: PackagePolicy) => { + const cloudShellUrl = packagePolicy?.inputs?.reduce((accInput, input) => { + if (accInput !== '') { + return accInput; + } + if (input?.enabled && input?.config?.cloud_shell_url) { + return input.config.cloud_shell_url.value; + } + return accInput; + }, ''); + + return cloudShellUrl !== '' ? cloudShellUrl : undefined; +}; diff --git a/x-pack/plugins/fleet/public/services/index.ts b/x-pack/plugins/fleet/public/services/index.ts index 44bf6b965742c..a98d4126d52f3 100644 --- a/x-pack/plugins/fleet/public/services/index.ts +++ b/x-pack/plugins/fleet/public/services/index.ts @@ -51,3 +51,5 @@ export { incrementPolicyName } from './increment_policy_name'; export { getCloudFormationPropsFromPackagePolicy } from './get_cloud_formation_props_from_package_policy'; export { getCloudFormationTemplateUrlFromAgentPolicy } from './get_cloud_formation_template_url_from_agent_policy'; export { getCloudFormationTemplateUrlFromPackageInfo } from './get_cloud_formation_template_url_from_package_info'; +export { getCloudShellUrlFromPackagePolicy } from './get_cloud_shell_url_from_package_policy'; +export { getCloudShellUrlFromAgentPolicy } from './get_cloud_shell_url_from_agent_policy'; diff --git a/x-pack/plugins/fleet/server/config.ts b/x-pack/plugins/fleet/server/config.ts index 14e5a86aa73ad..9726837375eed 100644 --- a/x-pack/plugins/fleet/server/config.ts +++ b/x-pack/plugins/fleet/server/config.ts @@ -139,7 +139,6 @@ export const config: PluginConfigDescriptor = { disableRegistryVersionCheck: schema.boolean({ defaultValue: false }), allowAgentUpgradeSourceUri: schema.boolean({ defaultValue: false }), bundledPackageLocation: schema.string({ defaultValue: DEFAULT_BUNDLED_PACKAGE_LOCATION }), - testSecretsIndex: schema.maybe(schema.string()), }), packageVerification: schema.object({ gpgKeyPath: schema.string({ defaultValue: DEFAULT_GPG_KEY_PATH }), diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts index e9a4e255e9ea1..ec0cbab539bcd 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts @@ -320,6 +320,7 @@ export async function installKibanaSavedObjects({ readStream: createListStream(toBeSavedObjects), createNewCopies: false, refresh: false, + managed: true, }) ); @@ -371,6 +372,7 @@ export async function installKibanaSavedObjects({ await savedObjectsImporter.resolveImportErrors({ readStream: createListStream(toBeSavedObjects), createNewCopies: false, + managed: true, retries, }); diff --git a/x-pack/plugins/fleet/server/services/preconfiguration/outputs.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration/outputs.test.ts index 8f62d3d7e2280..fda7789356956 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration/outputs.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration/outputs.test.ts @@ -64,6 +64,16 @@ describe('output preconfiguration', () => { hosts: ['http://es.co:80'], is_preconfigured: true, }, + { + id: 'existing-kafka-output-1', + is_default: false, + is_default_monitoring: false, + name: 'Kafka Output 1', + // @ts-ignore + type: 'kafka', + hosts: ['kafka.co:80'], + is_preconfigured: true, + }, ]; }); }); @@ -112,6 +122,25 @@ describe('output preconfiguration', () => { expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).not.toBeCalled(); }); + it('should create preconfigured kafka output that does not exists', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + await createOrUpdatePreconfiguredOutputs(soClient, esClient, [ + { + id: 'non-existing-kafka-output-1', + name: 'Output 1', + type: 'kafka', + is_default: false, + is_default_monitoring: false, + hosts: ['test.fr:2000'], + }, + ]); + + expect(mockedOutputService.create).toBeCalled(); + expect(mockedOutputService.update).not.toBeCalled(); + expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).not.toBeCalled(); + }); + it('should create a preconfigured output with ca_trusted_fingerprint that does not exists', async () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; @@ -238,6 +267,26 @@ describe('output preconfiguration', () => { expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).toBeCalled(); }); + it('should update output if preconfigured kafka output exists and changed', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + soClient.find.mockResolvedValue({ saved_objects: [], page: 0, per_page: 0, total: 0 }); + await createOrUpdatePreconfiguredOutputs(soClient, esClient, [ + { + id: 'existing-kafka-output-1', + is_default: false, + is_default_monitoring: false, + name: 'Kafka Output 1', + type: 'kafka', + hosts: ['kafka.co:8080'], + }, + ]); + + expect(mockedOutputService.create).not.toBeCalled(); + expect(mockedOutputService.update).toBeCalled(); + expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).toBeCalled(); + }); + it('should not update output if preconfigured output exists and did not changed', async () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; @@ -258,6 +307,26 @@ describe('output preconfiguration', () => { expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).toBeCalled(); }); + it('should not update output if preconfigured kafka output exists and did not change', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + soClient.find.mockResolvedValue({ saved_objects: [], page: 0, per_page: 0, total: 0 }); + await createOrUpdatePreconfiguredOutputs(soClient, esClient, [ + { + id: 'existing-kafka-output-1', + is_default: false, + is_default_monitoring: false, + name: 'Kafka Output 1', + type: 'kafka', + hosts: ['kafka.co:8080'], + }, + ]); + + expect(mockedOutputService.create).not.toBeCalled(); + expect(mockedOutputService.update).toBeCalled(); + expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).toBeCalled(); + }); + const SCENARIOS: Array<{ name: string; data: PreconfiguredOutput }> = [ { name: 'no changes', diff --git a/x-pack/plugins/fleet/server/services/preconfiguration/outputs.ts b/x-pack/plugins/fleet/server/services/preconfiguration/outputs.ts index 511e90d1e19a5..5f8f1a13feda9 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration/outputs.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration/outputs.ts @@ -168,6 +168,34 @@ function isPreconfiguredOutputDifferentFromCurrent( existingOutput: Output, preconfiguredOutput: Partial ): boolean { + const kafkaFieldsAreDifferent = (): boolean => { + if (existingOutput.type !== 'kafka' || preconfiguredOutput.type !== 'kafka') { + return false; + } + + return ( + isDifferent(existingOutput.client_id, preconfiguredOutput.client_id) || + isDifferent(existingOutput.version, preconfiguredOutput.version) || + isDifferent(existingOutput.key, preconfiguredOutput.key) || + isDifferent(existingOutput.compression, preconfiguredOutput.compression) || + isDifferent(existingOutput.compression_level, preconfiguredOutput.compression_level) || + isDifferent(existingOutput.auth_type, preconfiguredOutput.auth_type) || + isDifferent(existingOutput.connection_type, preconfiguredOutput.connection_type) || + isDifferent(existingOutput.username, preconfiguredOutput.username) || + isDifferent(existingOutput.password, preconfiguredOutput.password) || + isDifferent(existingOutput.sasl, preconfiguredOutput.sasl) || + isDifferent(existingOutput.partition, preconfiguredOutput.partition) || + isDifferent(existingOutput.random, preconfiguredOutput.random) || + isDifferent(existingOutput.round_robin, preconfiguredOutput.round_robin) || + isDifferent(existingOutput.hash, preconfiguredOutput.hash) || + isDifferent(existingOutput.topics, preconfiguredOutput.topics) || + isDifferent(existingOutput.headers, preconfiguredOutput.headers) || + isDifferent(existingOutput.timeout, preconfiguredOutput.timeout) || + isDifferent(existingOutput.broker_timeout, preconfiguredOutput.broker_timeout) || + isDifferent(existingOutput.required_acks, preconfiguredOutput.required_acks) + ); + }; + return ( !existingOutput.is_preconfigured || isDifferent(existingOutput.is_default, preconfiguredOutput.is_default) || @@ -191,6 +219,7 @@ function isPreconfiguredOutputDifferentFromCurrent( ) || isDifferent(existingOutput.config_yaml, preconfiguredOutput.config_yaml) || isDifferent(existingOutput.proxy_id, preconfiguredOutput.proxy_id) || - isDifferent(existingOutput.allow_edit ?? [], preconfiguredOutput.allow_edit ?? []) + isDifferent(existingOutput.allow_edit ?? [], preconfiguredOutput.allow_edit ?? []) || + kafkaFieldsAreDifferent() ); } diff --git a/x-pack/plugins/index_management/public/application/mount_management_section.ts b/x-pack/plugins/index_management/public/application/mount_management_section.ts index d00aa6ff1f0e6..6bb3b834ce85f 100644 --- a/x-pack/plugins/index_management/public/application/mount_management_section.ts +++ b/x-pack/plugins/index_management/public/application/mount_management_section.ts @@ -53,7 +53,7 @@ export async function mountManagementSection( extensionsService: ExtensionsService, isFleetEnabled: boolean, kibanaVersion: SemVer, - enableIndexActions: boolean + enableIndexActions: boolean = true ) { const { element, setBreadcrumbs, history, theme$ } = params; const [core, startDependencies] = await coreSetup.getStartServices(); diff --git a/x-pack/plugins/index_management/public/types.ts b/x-pack/plugins/index_management/public/types.ts index 59954c6659494..20d2405a0fa4b 100644 --- a/x-pack/plugins/index_management/public/types.ts +++ b/x-pack/plugins/index_management/public/types.ts @@ -28,5 +28,5 @@ export interface ClientConfigType { ui: { enabled: boolean; }; - enableIndexActions: boolean; + enableIndexActions?: boolean; } diff --git a/x-pack/plugins/index_management/server/config.ts b/x-pack/plugins/index_management/server/config.ts index 4fd24bf3fcdf7..c5d459486a8ef 100644 --- a/x-pack/plugins/index_management/server/config.ts +++ b/x-pack/plugins/index_management/server/config.ts @@ -22,7 +22,14 @@ const schemaLatest = schema.object( ui: schema.object({ enabled: schema.boolean({ defaultValue: true }), }), - enableIndexActions: schema.boolean({ defaultValue: true }), + enableIndexActions: schema.conditional( + schema.contextRef('serverless'), + true, + // Index actions are disabled in serverless; refer to the serverless.yml file as the source of truth + // We take this approach in order to have a central place (serverless.yml) for serverless config across Kibana + schema.boolean({ defaultValue: true }), + schema.never() + ), }, { defaultValue: undefined } ); diff --git a/x-pack/plugins/infra/public/components/asset_details/__stories__/context/fixtures/asset_details_state.ts b/x-pack/plugins/infra/public/components/asset_details/__stories__/context/fixtures/asset_details_state.ts index edcd1d0627d3d..4e88dc368ca0a 100644 --- a/x-pack/plugins/infra/public/components/asset_details/__stories__/context/fixtures/asset_details_state.ts +++ b/x-pack/plugins/infra/public/components/asset_details/__stories__/context/fixtures/asset_details_state.ts @@ -9,7 +9,7 @@ import type { DataViewField, DataView } from '@kbn/data-views-plugin/common'; import { UseAssetDetailsStateProps } from '../../../hooks/use_asset_details_state'; export const assetDetailsState: UseAssetDetailsStateProps['state'] = { - node: { + asset: { name: 'host1', id: 'host1-macOS', ip: '192.168.0.1', @@ -29,7 +29,7 @@ export const assetDetailsState: UseAssetDetailsStateProps['state'] = { showActionsColumn: true, }, }, - nodeType: 'host', + assetType: 'host', dateRange: { from: '2023-04-09T11:07:49Z', to: '2023-04-09T11:23:49Z', diff --git a/x-pack/plugins/infra/public/components/asset_details/__stories__/decorator.tsx b/x-pack/plugins/infra/public/components/asset_details/__stories__/decorator.tsx index 836cd86ce5d54..e0a9ca3ec8679 100644 --- a/x-pack/plugins/infra/public/components/asset_details/__stories__/decorator.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/__stories__/decorator.tsx @@ -115,6 +115,9 @@ export const DecorateWithKibanaContext: DecoratorFn = (story) => { navigateToPrefilledEditor: () => {}, stateHelperApi: () => new Promise(() => {}), }, + telemetry: { + reportAssetDetailsFlyoutViewed: () => {}, + }, }; return ( diff --git a/x-pack/plugins/infra/public/components/asset_details/asset_details.stories.tsx b/x-pack/plugins/infra/public/components/asset_details/asset_details.stories.tsx index a90fe5764f531..824a4e5f65ef0 100644 --- a/x-pack/plugins/infra/public/components/asset_details/asset_details.stories.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/asset_details.stories.tsx @@ -22,42 +22,36 @@ const tabs: Tab[] = [ name: i18n.translate('xpack.infra.nodeDetails.tabs.overview.title', { defaultMessage: 'Overview', }), - 'data-test-subj': 'hostsView-flyout-tabs-overview', }, { id: FlyoutTabIds.LOGS, name: i18n.translate('xpack.infra.nodeDetails.tabs.logs', { defaultMessage: 'Logs', }), - 'data-test-subj': 'hostsView-flyout-tabs-logs', }, { id: FlyoutTabIds.METADATA, name: i18n.translate('xpack.infra.metrics.nodeDetails.tabs.metadata', { defaultMessage: 'Metadata', }), - 'data-test-subj': 'hostsView-flyout-tabs-metadata', }, { id: FlyoutTabIds.PROCESSES, name: i18n.translate('xpack.infra.metrics.nodeDetails.tabs.processes', { defaultMessage: 'Processes', }), - 'data-test-subj': 'hostsView-flyout-tabs-processes', }, { id: FlyoutTabIds.ANOMALIES, name: i18n.translate('xpack.infra.nodeDetails.tabs.anomalies', { defaultMessage: 'Anomalies', }), - 'data-test-subj': 'hostsView-flyout-tabs-anomalies', }, { id: FlyoutTabIds.LINK_TO_APM, name: i18n.translate('xpack.infra.infra.nodeDetails.apmTabLabel', { defaultMessage: 'APM', }), - 'data-test-subj': 'hostsView-flyout-apm-link', }, ]; @@ -96,7 +90,7 @@ const FlyoutTemplate: Story = (args) => { Open flyout
); @@ -107,7 +101,7 @@ export const Page = PageTemplate.bind({}); export const Flyout = FlyoutTemplate.bind({}); Flyout.args = { renderMode: { - showInFlyout: true, + mode: 'flyout', closeFlyout: () => {}, }, }; diff --git a/x-pack/plugins/infra/public/components/asset_details/asset_details.tsx b/x-pack/plugins/infra/public/components/asset_details/asset_details.tsx index 101a23a4d084e..e55a222b54719 100644 --- a/x-pack/plugins/infra/public/components/asset_details/asset_details.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/asset_details.tsx @@ -7,11 +7,17 @@ import React from 'react'; import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody } from '@elastic/eui'; +import useEffectOnce from 'react-use/lib/useEffectOnce'; import type { AssetDetailsProps, RenderMode } from './types'; import { Content } from './content/content'; import { Header } from './header/header'; -import { TabSwitcherProvider } from './hooks/use_tab_switcher'; -import { AssetDetailsStateProvider } from './hooks/use_asset_details_state'; +import { TabSwitcherProvider, useTabSwitcherContext } from './hooks/use_tab_switcher'; +import { + AssetDetailsStateProvider, + useAssetDetailsStateContext, +} from './hooks/use_asset_details_state'; +import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; +import { ASSET_DETAILS_FLYOUT_COMPONENT_NAME } from './constants'; interface ContentTemplateProps { header: React.ReactElement; @@ -20,8 +26,27 @@ interface ContentTemplateProps { } const ContentTemplate = ({ header, body, renderMode }: ContentTemplateProps) => { - return renderMode.showInFlyout ? ( - + const { assetType } = useAssetDetailsStateContext(); + const { initialActiveTabId } = useTabSwitcherContext(); + const { + services: { telemetry }, + } = useKibanaContextForPlugin(); + + useEffectOnce(() => { + telemetry.reportAssetDetailsFlyoutViewed({ + componentName: ASSET_DETAILS_FLYOUT_COMPONENT_NAME, + assetType, + tabId: initialActiveTabId, + }); + }); + + return renderMode.mode === 'flyout' ? ( + {header} {body} @@ -34,25 +59,34 @@ const ContentTemplate = ({ header, body, renderMode }: ContentTemplateProps) => }; export const AssetDetails = ({ - node, + asset, dateRange, activeTabId, overrides, onTabsStateChange, tabs = [], links = [], - nodeType = 'host', + assetType = 'host', renderMode = { - showInFlyout: false, + mode: 'page', }, }: AssetDetailsProps) => { return ( - + 0 ? activeTabId ?? tabs[0].id : undefined} > } + header={
} body={} renderMode={renderMode} /> diff --git a/x-pack/plugins/infra/public/components/asset_details/asset_details_embeddable.tsx b/x-pack/plugins/infra/public/components/asset_details/asset_details_embeddable.tsx index 355d8fac0055b..534ba4c2e4265 100644 --- a/x-pack/plugins/infra/public/components/asset_details/asset_details_embeddable.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/asset_details_embeddable.tsx @@ -73,8 +73,8 @@ export class AssetDetailsEmbeddable extends Embeddable { values={{ documentation: ( diff --git a/x-pack/plugins/infra/public/components/asset_details/components/expandable_content.tsx b/x-pack/plugins/infra/public/components/asset_details/components/expandable_content.tsx index 60f487d52d0ea..0cd5e1a53013a 100644 --- a/x-pack/plugins/infra/public/components/asset_details/components/expandable_content.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/components/expandable_content.tsx @@ -31,7 +31,10 @@ export const ExpandableContent = (props: ExpandableContentProps) => { {shouldShowMore && ( <> {' ... '} - + & { const APM_FIELD = 'host.hostname'; export const Header = ({ tabs = [], links = [], compact }: Props) => { - const { node, nodeType, overrides, dateRange: timeRange } = useAssetDetailsStateContext(); + const { asset, assetType, overrides, dateRange: timeRange } = useAssetDetailsStateContext(); const { euiTheme } = useEuiTheme(); const { showTab, activeTabId } = useTabSwitcherContext(); @@ -46,23 +47,23 @@ export const Header = ({ tabs = [], links = [], compact }: Props) => { const tabLinkComponents = { [FlyoutTabIds.LINK_TO_APM]: (tab: Tab) => ( - + ), [FlyoutTabIds.LINK_TO_UPTIME]: (tab: Tab) => ( - + ), }; const topCornerLinkComponents: Record = { nodeDetails: ( ), alertRule: , - apmServices: , + apmServices: , }; const tabEntries = tabs.map(({ name, ...tab }) => { @@ -77,6 +78,7 @@ export const Header = ({ tabs = [], links = [], compact }: Props) => { return ( onTabClick(tab.id)} isSelected={tab.id === activeTabId} @@ -102,7 +104,7 @@ export const Header = ({ tabs = [], links = [], compact }: Props) => { `} > - {compact ?

{node.name}

:

{node.name}

} + {compact ?

{asset.name}

:

{asset.name}

}
; } export function useAssetDetailsState({ state }: UseAssetDetailsStateProps) { - const { node, nodeType, dateRange: rawDateRange, onTabsStateChange, overrides } = state; + const { + asset, + assetType, + dateRange: rawDateRange, + onTabsStateChange, + overrides, + renderMode, + } = state; const dateRange = useMemo(() => { const { from = DEFAULT_DATE_RANGE.from, to = DEFAULT_DATE_RANGE.to } = @@ -35,13 +45,27 @@ export function useAssetDetailsState({ state }: UseAssetDetailsStateProps) { const dateRangeTs = toTimestampRange(dateRange); + const inventoryModel = findInventoryModel(assetType); + const { sourceId } = useSourceContext(); + const { + loading: metadataLoading, + error: fetchMetadataError, + metadata, + } = useMetadata(asset.name, assetType, inventoryModel.requiredMetrics, sourceId, dateRangeTs); + return { - node, - nodeType, + asset, + assetType, dateRange, dateRangeTs, onTabsStateChange, overrides, + renderMode, + metadataResponse: { + metadataLoading, + fetchMetadataError, + metadata, + }, }; } diff --git a/x-pack/plugins/infra/public/components/asset_details/hooks/use_tab_switcher.tsx b/x-pack/plugins/infra/public/components/asset_details/hooks/use_tab_switcher.tsx index 60dc710b8613f..6bdcbca214d37 100644 --- a/x-pack/plugins/infra/public/components/asset_details/hooks/use_tab_switcher.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/hooks/use_tab_switcher.tsx @@ -33,6 +33,7 @@ export function useTabSwitcher({ initialActiveTabId }: TabSwitcherParams) { }; return { + initialActiveTabId, activeTabId, renderedTabsSet, showTab, diff --git a/x-pack/plugins/infra/public/components/asset_details/links/link_to_alerts.tsx b/x-pack/plugins/infra/public/components/asset_details/links/link_to_alerts.tsx index 47d7075cb60dc..e5a5cc6340abe 100644 --- a/x-pack/plugins/infra/public/components/asset_details/links/link_to_alerts.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/links/link_to_alerts.tsx @@ -15,7 +15,7 @@ export interface LinkToAlertsRuleProps { export const LinkToAlertsRule = ({ onClick }: LinkToAlertsRuleProps) => { return ( { +export const LinkToAlertsPage = ({ assetName, queryField, dateRange }: LinkToAlertsPageProps) => { const { services } = useKibanaContextForPlugin(); const { http } = services; const linkToAlertsPage = http.basePath.prepend( `${ALERTS_PATH}?_a=${encode({ - kuery: `${queryField}:"${nodeName}"`, + kuery: `${queryField}:"${assetName}"`, rangeFrom: dateRange.from, rangeTo: dateRange.to, status: 'all', @@ -35,7 +35,7 @@ export const LinkToAlertsPage = ({ nodeName, queryField, dateRange }: LinkToAler return ( { +export const LinkToApmServices = ({ assetName, apmField }: LinkToApmServicesProps) => { const { services } = useKibanaContextForPlugin(); const { http } = services; const queryString = new URLSearchParams( encode( stringify({ - kuery: `${apmField}:"${nodeName}"`, + kuery: `${apmField}:"${assetName}"`, }) ) ); @@ -34,7 +34,7 @@ export const LinkToApmServices = ({ nodeName, apmField }: LinkToApmServicesProps return ( { - const inventoryModel = findInventoryModel(nodeType); + const inventoryModel = findInventoryModel(assetType); const nodeDetailFrom = currentTimestamp - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000; const nodeDetailMenuItemLinkProps = useLinkProps({ ...getNodeDetailUrl({ - nodeType, - nodeId: nodeName, + nodeType: assetType, + nodeId: assetName, from: nodeDetailFrom, to: currentTimestamp, }), @@ -37,7 +37,7 @@ export const LinkToNodeDetails = ({ return ( { +export const TabToApmTraces = ({ assetName, apmField, name, ...props }: LinkToApmServicesProps) => { const { euiTheme } = useEuiTheme(); const apmTracesMenuItemLinkProps = useLinkProps({ app: 'apm', hash: 'traces', search: { - kuery: `${apmField}:"${nodeName}"`, + kuery: `${apmField}:"${assetName}"`, }, }); @@ -30,7 +30,7 @@ export const TabToApmTraces = ({ nodeName, apmField, name, ...props }: LinkToApm { +export const TabToUptime = ({ + assetType, + assetName, + nodeIp, + name, + ...props +}: LinkToUptimeProps) => { const { share } = useKibanaContextForPlugin().services; const { euiTheme } = useEuiTheme(); return ( share.url.locators .get(uptimeOverviewLocatorID)! - .navigate({ [nodeType]: nodeName, ip: nodeIp }) + .navigate({ [assetType]: assetName, ip: nodeIp }) } > { - const { node, overrides } = useAssetDetailsStateContext(); + const { asset, overrides } = useAssetDetailsStateContext(); const { onClose = () => {} } = overrides?.anomalies ?? {}; - return ; + return ; }; diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx index 087c34551c440..35337032805c1 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx @@ -22,7 +22,7 @@ import { useAssetDetailsStateContext } from '../../hooks/use_asset_details_state const TEXT_QUERY_THROTTLE_INTERVAL_MS = 500; export const Logs = () => { - const { node, nodeType, overrides, onTabsStateChange, dateRangeTs } = + const { asset, assetType, overrides, onTabsStateChange, dateRangeTs } = useAssetDetailsStateContext(); const { logView: overrideLogView, query: overrideQuery } = overrides?.logs ?? {}; @@ -49,7 +49,7 @@ export const Logs = () => { const filter = useMemo(() => { const query = [ - `${findInventoryFields(nodeType).id}: "${node.name}"`, + `${findInventoryFields(assetType).id}: "${asset.name}"`, ...(textQueryDebounced !== '' ? [textQueryDebounced] : []), ].join(' and '); @@ -57,7 +57,7 @@ export const Logs = () => { language: 'kuery', query, }; - }, [nodeType, node.name, textQueryDebounced]); + }, [assetType, asset.name, textQueryDebounced]); const onQueryChange = useCallback((e: React.ChangeEvent) => { setTextQuery(e.target.value); @@ -70,13 +70,20 @@ export const Logs = () => { const logsUrl = useMemo(() => { return locators.nodeLogsLocator.getRedirectUrl({ - nodeType, - nodeId: node.name, + nodeType: assetType, + nodeId: asset.name, time: startTimestamp, filter: textQueryDebounced, logView, }); - }, [locators.nodeLogsLocator, node.name, nodeType, startTimestamp, textQueryDebounced, logView]); + }, [ + locators.nodeLogsLocator, + asset.name, + assetType, + startTimestamp, + textQueryDebounced, + logView, + ]); return ( diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/add_metadata_filter_button.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/add_metadata_filter_button.tsx index 31d978235be32..a2d04b1c36184 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/add_metadata_filter_button.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/add_metadata_filter_button.tsx @@ -75,7 +75,7 @@ export const AddMetadataFilterButton = ({ item }: AddMetadataFilterButtonProps) color="text" iconType="filter" display="base" - data-test-subj="hostsView-flyout-metadata-remove-filter" + data-test-subj="infraAssetDetailsMetadataRemoveFilterButton" aria-label={i18n.translate('xpack.infra.metadataEmbeddable.filterAriaLabel', { defaultMessage: 'Filter', })} @@ -102,7 +102,7 @@ export const AddMetadataFilterButton = ({ item }: AddMetadataFilterButtonProps) color="primary" size="s" iconType="filter" - data-test-subj="hostsView-flyout-metadata-add-filter" + data-test-subj="infraAssetDetailsMetadataAddFilterButton" aria-label={i18n.translate('xpack.infra.metadataEmbeddable.AddFilterAriaLabel', { defaultMessage: 'Add Filter', })} diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/add_pin_to_row.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/add_pin_to_row.tsx index a1e7c3f106497..1e5e31b887911 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/add_pin_to_row.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/add_pin_to_row.tsx @@ -47,8 +47,8 @@ export const AddMetadataPinToRow = ({ size="s" color="primary" iconType="pinFilled" - data-test-subj="infraMetadataEmbeddableRemovePin" - aria-label={i18n.translate('xpack.infra.metadataEmbeddable.pinAriaLabel', { + data-test-subj="infraAssetDetailsMetadataRemovePin" + aria-label={i18n.translate('xpack.infra.metadata.pinAriaLabel', { defaultMessage: 'Pinned field', })} onClick={handleRemovePin} @@ -65,7 +65,7 @@ export const AddMetadataPinToRow = ({ color="primary" size="s" iconType="pin" - data-test-subj="infraMetadataEmbeddableAddPin" + data-test-subj="infraAssetDetailsMetadataAddPin" aria-label={PIN_FIELD} onClick={handleAddPin} /> diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.test.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.test.tsx index eeff58d30d1e7..9f8a04ef64e6c 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.test.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.test.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { Metadata } from './metadata'; - import { useMetadata } from '../../hooks/use_metadata'; import { useSourceContext } from '../../../../containers/metrics_source'; import { render } from '@testing-library/react'; @@ -27,8 +26,8 @@ const renderHostMetadata = () => from: '2023-04-09T11:07:49Z', to: '2023-04-09T11:23:49Z', }, - nodeType: 'host', - node: { + assetType: 'host', + asset: { id: 'host-1', name: 'host-1', }, @@ -69,30 +68,30 @@ describe('Single Host Metadata (Hosts View)', () => { mockUseMetadata({ error: 'Internal server error' }); const result = renderHostMetadata(); - expect(result.queryByTestId('infraMetadataErrorCallout')).toBeInTheDocument(); + expect(result.queryByTestId('infraAssetDetailsMetadataErrorCallout')).toBeInTheDocument(); }); it('should show an no data message if fetching the metadata returns an empty array', async () => { mockUseMetadata({ metadata: [] }); const result = renderHostMetadata(); - expect(result.queryByTestId('infraHostMetadataSearchBarInput')).toBeInTheDocument(); - expect(result.queryByTestId('infraHostMetadataNoData')).toBeInTheDocument(); + expect(result.queryByTestId('infraAssetDetailsMetadataSearchBarInput')).toBeInTheDocument(); + expect(result.queryByTestId('infraAssetDetailsMetadataNoData')).toBeInTheDocument(); }); it('should show the metadata table if metadata is returned', async () => { mockUseMetadata({ metadata: [{ name: 'host.os.name', value: 'Ubuntu' }] }); const result = renderHostMetadata(); - expect(result.queryByTestId('infraHostMetadataSearchBarInput')).toBeInTheDocument(); - expect(result.queryByTestId('infraMetadataTable')).toBeInTheDocument(); + expect(result.queryByTestId('infraAssetDetailsMetadataSearchBarInput')).toBeInTheDocument(); + expect(result.queryByTestId('infraAssetDetailsMetadataTable')).toBeInTheDocument(); }); it('should return loading text if loading', async () => { mockUseMetadata({ loading: true }); const result = renderHostMetadata(); - expect(result.queryByTestId('infraHostMetadataSearchBarInput')).toBeInTheDocument(); - expect(result.queryByTestId('infraHostMetadataLoading')).toBeInTheDocument(); + expect(result.queryByTestId('infraAssetDetailsMetadataSearchBarInput')).toBeInTheDocument(); + expect(result.queryByTestId('infraAssetDetailsMetadataLoading')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.tsx index 23a3e5f193409..70f1bda8c0c68 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.tsx @@ -11,9 +11,6 @@ import { EuiCallOut, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { TimeRange } from '@kbn/es-query'; import type { InventoryItemType } from '../../../../../common/inventory_models/types'; -import { findInventoryModel } from '../../../../../common/inventory_models'; -import { useMetadata } from '../../hooks/use_metadata'; -import { useSourceContext } from '../../../../containers/metrics_source'; import { Table } from './table'; import { getAllFields } from './utils'; import { useAssetDetailsStateContext } from '../../hooks/use_asset_details_state'; @@ -24,26 +21,18 @@ export interface MetadataSearchUrlState { } export interface MetadataProps { + assetName: string; + assetType: InventoryItemType; dateRange: TimeRange; - nodeName: string; - nodeType: InventoryItemType; showActionsColumn?: boolean; search?: string; onSearchChange?: (query: string) => void; } export const Metadata = () => { - const { node, nodeType, overrides, dateRangeTs, onTabsStateChange } = - useAssetDetailsStateContext(); + const { overrides, onTabsStateChange, metadataResponse } = useAssetDetailsStateContext(); const { query, showActionsColumn = false } = overrides?.metadata ?? {}; - - const inventoryModel = findInventoryModel(nodeType); - const { sourceId } = useSourceContext(); - const { - loading: metadataLoading, - error: fetchMetadataError, - metadata, - } = useMetadata(node.name, nodeType, inventoryModel.requiredMetrics, sourceId, dateRangeTs); + const { metadataLoading, fetchMetadataError, metadata } = metadataResponse; const fields = useMemo(() => getAllFields(metadata), [metadata]); @@ -64,7 +53,7 @@ export const Metadata = () => { })} color="danger" iconType="error" - data-test-subj="infraMetadataErrorCallout" + data-test-subj="infraAssetDetailsMetadataErrorCallout" > {LOADING}

+
{LOADING}
) : ( -
{NO_METADATA_FOUND}
+
{NO_METADATA_FOUND}
) } /> diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/osquery/osquery.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/osquery/osquery.tsx index 06640540af16d..f2809c86ae5b9 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/osquery/osquery.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/osquery/osquery.tsx @@ -8,22 +8,12 @@ import { EuiSkeletonText } from '@elastic/eui'; import React, { useMemo } from 'react'; import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; -import { useSourceContext } from '../../../../containers/metrics_source'; -import { findInventoryModel } from '../../../../../common/inventory_models'; -import { useMetadata } from '../../hooks/use_metadata'; import { useAssetDetailsStateContext } from '../../hooks/use_asset_details_state'; export const Osquery = () => { - const { node, nodeType, dateRangeTs } = useAssetDetailsStateContext(); - const inventoryModel = findInventoryModel(nodeType); - const { sourceId } = useSourceContext(); - const { loading, metadata } = useMetadata( - node.name, - nodeType, - inventoryModel.requiredMetrics, - sourceId, - dateRangeTs - ); + const { metadataResponse } = useAssetDetailsStateContext(); + + const { metadataLoading, metadata } = metadataResponse; const { services: { osquery }, } = useKibanaContextForPlugin(); @@ -34,12 +24,12 @@ export const Osquery = () => { // avoids component rerender when resizing the popover const content = useMemo(() => { // TODO: Add info when Osquery plugin is not available - if (loading || !OsqueryAction) { + if (metadataLoading || !OsqueryAction) { return ; } return ; - }, [OsqueryAction, loading, metadata]); + }, [OsqueryAction, metadataLoading, metadata]); return content; }; diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts.tsx index 87ebaae1f5f20..2edac4abbbdda 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts.tsx @@ -25,12 +25,12 @@ import { useBoolean } from '../../../../hooks/use_boolean'; import { ALERT_STATUS_ALL } from '../../../../common/alerts/constants'; export const AlertsSummaryContent = ({ - nodeName, - nodeType, + assetName, + assetType, dateRange, }: { - nodeName: string; - nodeType: InventoryItemType; + assetName: string; + assetType: InventoryItemType; dateRange: TimeRange; }) => { const [isAlertFlyoutVisible, { toggle: toggleAlertFlyout }] = useBoolean(false); @@ -39,10 +39,10 @@ export const AlertsSummaryContent = ({ () => createAlertsEsQuery({ dateRange, - hostNodeNames: [nodeName], + hostNodeNames: [assetName], status: ALERT_STATUS_ALL, }), - [nodeName, dateRange] + [assetName, dateRange] ); return ( @@ -56,8 +56,8 @@ export const AlertsSummaryContent = ({ @@ -65,8 +65,8 @@ export const AlertsSummaryContent = ({ @@ -112,7 +112,7 @@ const AlertsSectionTitle = () => { return ( - +
{ diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/kpis/kpi_grid.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/kpis/kpi_grid.tsx index c134f0de6bb7a..dfc7f8823ba71 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/kpis/kpi_grid.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/kpis/kpi_grid.tsx @@ -35,7 +35,7 @@ export const KPIGrid = React.memo(({ nodeName, dataView, timeRange }: Props) => }, [dataView, nodeName]); return ( - + {KPI_CHARTS.map(({ id, layers, title, toolTip }, index) => ( { @@ -72,7 +84,7 @@ export const MetadataHeader = ({ metadataValue }: MetadataSummaryProps) => { values={{ documentation: ( diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_summary_list.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_summary_list.tsx index 1aedec3f05037..bb5e0a637483d 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_summary_list.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_summary_list.tsx @@ -25,6 +25,7 @@ import { MetadataHeader } from './metadata_header'; interface MetadataSummaryProps { metadata: InfraMetadata | null; metadataLoading: boolean; + isCompactView: boolean; } export interface MetadataData { @@ -34,6 +35,20 @@ export interface MetadataData { tooltipLink?: string; } +const extendedMetadata = (metadataInfo: InfraMetadata['info']): MetadataData[] => [ + { + field: 'cloudProvider', + value: metadataInfo?.cloud?.provider, + tooltipFieldLabel: 'cloud.provider', + tooltipLink: 'https://www.elastic.co/guide/en/ecs/current/ecs-cloud.html#field-cloud-provider', + }, + { + field: 'operatingSystem', + value: metadataInfo?.host?.os?.name, + tooltipFieldLabel: 'host.os.name', + }, +]; + const metadataData = (metadataInfo: InfraMetadata['info']): MetadataData[] => [ { field: 'hostIp', @@ -48,7 +63,11 @@ const metadataData = (metadataInfo: InfraMetadata['info']): MetadataData[] => [ }, ]; -export const MetadataSummaryList = ({ metadata, metadataLoading }: MetadataSummaryProps) => { +export const MetadataSummaryList = ({ + metadata, + metadataLoading, + isCompactView, +}: MetadataSummaryProps) => { const { showTab } = useTabSwitcherContext(); const onClick = () => { @@ -58,7 +77,10 @@ export const MetadataSummaryList = ({ metadata, metadataLoading }: MetadataSumma return ( - {metadataData(metadata?.info).map( + {(isCompactView + ? metadataData(metadata?.info) + : [...metadataData(metadata?.info), ...extendedMetadata(metadata?.info)] + ).map( (metadataValue) => metadataValue && ( @@ -78,7 +100,7 @@ export const MetadataSummaryList = ({ metadata, metadataLoading }: MetadataSumma - + {CHARTS_IN_ORDER.map(({ dataViewOrigin, id, layers, title, overrides }, index) => ( { - const { node, nodeType, overrides, dateRange } = useAssetDetailsStateContext(); + const { asset, assetType, overrides, dateRange, renderMode, metadataResponse } = + useAssetDetailsStateContext(); const { logsDataView, metricsDataView } = overrides?.overview ?? {}; - const inventoryModel = findInventoryModel(nodeType); - const { sourceId } = useSourceContext(); - const { - loading: metadataLoading, - error: fetchMetadataError, - metadata, - } = useMetadata( - node.name, - nodeType, - inventoryModel.requiredMetrics, - sourceId, - toTimestampRange(dateRange) - ); + const { metadataLoading, fetchMetadataError, metadata } = metadataResponse; return ( - + {fetchMetadataError ? ( @@ -59,7 +44,7 @@ export const Overview = () => { values={{ reload: ( window.location.reload()} > {i18n.translate('xpack.infra.assetDetailsEmbeddable.overview.errorAction', { @@ -71,12 +56,16 @@ export const Overview = () => { /> ) : ( - + )} - + @@ -84,7 +73,7 @@ export const Overview = () => { timeRange={dateRange} logsDataView={logsDataView} metricsDataView={metricsDataView} - nodeName={node.name} + nodeName={asset.name} /> diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes.tsx index b91c96c3c948e..7bb8e96276fd8 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes.tsx @@ -35,7 +35,7 @@ const options = Object.entries(STATE_NAMES).map(([value, view]: [string, string] })); export const Processes = () => { - const { node, nodeType, overrides, dateRangeTs, onTabsStateChange } = + const { asset, assetType, overrides, dateRangeTs, onTabsStateChange } = useAssetDetailsStateContext(); const { query: overrideQuery } = overrides?.processes ?? {}; @@ -52,9 +52,9 @@ export const Processes = () => { }); const hostTerm = useMemo(() => { - const field = getFieldByType(nodeType) ?? nodeType; - return { [field]: node.name }; - }, [node.name, nodeType]); + const field = getFieldByType(assetType) ?? assetType; + return { [field]: asset.name }; + }, [asset.name, assetType]); const { loading, @@ -159,7 +159,7 @@ export const Processes = () => { } actions={ - + {columns.map((column) => ( diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/summary_table.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/summary_table.tsx index 57814aa7bacc2..928f48307eee7 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/summary_table.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/summary_table.tsx @@ -59,7 +59,10 @@ export const SummaryTable = ({ processSummary, isLoading }: Props) => { {Object.entries(processCount).map(([field, value]) => ( - + {columnTitles[field as keyof SummaryRecord]} {value === -1 ? : value} diff --git a/x-pack/plugins/infra/public/components/asset_details/types.ts b/x-pack/plugins/infra/public/components/asset_details/types.ts index ebd3c8823b8ca..cbf66d66f0a27 100644 --- a/x-pack/plugins/infra/public/components/asset_details/types.ts +++ b/x-pack/plugins/infra/public/components/asset_details/types.ts @@ -13,7 +13,7 @@ import type { InventoryItemType } from '../../../common/inventory_models/types'; interface Metadata { ip?: string | null; } -export type Node = Metadata & { +export type Asset = Metadata & { id: string; name: string; }; @@ -60,11 +60,11 @@ export interface TabState { export interface FlyoutProps { closeFlyout: () => void; - showInFlyout: true; + mode: 'flyout'; } export interface FullPageProps { - showInFlyout: false; + mode: 'page'; } export type RenderMode = FlyoutProps | FullPageProps; @@ -72,14 +72,13 @@ export type RenderMode = FlyoutProps | FullPageProps; export interface Tab { id: FlyoutTabIds; name: string; - 'data-test-subj': string; } export type LinkOptions = 'alertRule' | 'nodeDetails' | 'apmServices'; export interface AssetDetailsProps { - node: Node; - nodeType: InventoryItemType; + asset: Asset; + assetType: InventoryItemType; dateRange: TimeRange; tabs: Tab[]; activeTabId?: TabIds; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/flyout_wrapper.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/flyout_wrapper.tsx index a1d8542130c14..884e0dd389cd3 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/flyout_wrapper.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/flyout_wrapper.tsx @@ -7,7 +7,6 @@ import React from 'react'; import useAsync from 'react-use/lib/useAsync'; -import type { InventoryItemType } from '../../../../../../common/inventory_models/types'; import { useUnifiedSearchContext } from '../../hooks/use_unified_search'; import type { HostNodeRow } from '../../hooks/use_hosts_table'; import { HostFlyout, useHostFlyoutUrlState } from '../../hooks/use_host_flyout_url_state'; @@ -21,8 +20,6 @@ export interface Props { closeFlyout: () => void; } -const NODE_TYPE = 'host' as InventoryItemType; - export const FlyoutWrapper = ({ node, closeFlyout }: Props) => { const { searchCriteria } = useUnifiedSearchContext(); const { dataView } = useMetricsDataViewContext(); @@ -39,8 +36,8 @@ export const FlyoutWrapper = ({ node, closeFlyout }: Props) => { return ( { tabs={orderedFlyoutTabs} links={['apmServices', 'nodeDetails']} renderMode={{ - showInFlyout: true, + mode: 'flyout', closeFlyout, }} /> diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/tabs.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/tabs.ts index 4445e5fba924a..7d354d19bed12 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/tabs.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/tabs.ts @@ -14,41 +14,35 @@ export const orderedFlyoutTabs: Tab[] = [ name: i18n.translate('xpack.infra.nodeDetails.tabs.overview.title', { defaultMessage: 'Overview', }), - 'data-test-subj': 'hostsView-flyout-tabs-overview', }, { id: FlyoutTabIds.METADATA, name: i18n.translate('xpack.infra.nodeDetails.tabs.metadata.title', { defaultMessage: 'Metadata', }), - 'data-test-subj': 'hostsView-flyout-tabs-metadata', }, { id: FlyoutTabIds.PROCESSES, name: i18n.translate('xpack.infra.metrics.nodeDetails.tabs.processes', { defaultMessage: 'Processes', }), - 'data-test-subj': 'hostsView-flyout-tabs-processes', }, { id: FlyoutTabIds.LOGS, name: i18n.translate('xpack.infra.nodeDetails.tabs.logs.title', { defaultMessage: 'Logs', }), - 'data-test-subj': 'hostsView-flyout-tabs-logs', }, { id: FlyoutTabIds.ANOMALIES, name: i18n.translate('xpack.infra.nodeDetails.tabs.anomalies', { defaultMessage: 'Anomalies', }), - 'data-test-subj': 'hostsView-flyout-tabs-anomalies', }, { id: FlyoutTabIds.OSQUERY, name: i18n.translate('xpack.infra.nodeDetails.tabs.osquery', { defaultMessage: 'Osquery', }), - 'data-test-subj': 'hostsView-flyout-tabs-Osquery', }, ]; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx index e3ca9b770e2ef..6806fbd31d79a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx @@ -86,6 +86,7 @@ export const WaffleInventorySwitcher: React.FC = () => { { name: 'AWS', panel: 'awsPanel', + 'data-test-subj': 'goToAWS-open', }, ], }, @@ -96,18 +97,22 @@ export const WaffleInventorySwitcher: React.FC = () => { { name: getDisplayNameForType('awsEC2'), onClick: goToAwsEC2, + 'data-test-subj': 'goToAWS-EC2', }, { name: getDisplayNameForType('awsS3'), onClick: goToAwsS3, + 'data-test-subj': 'goToAWS-S3', }, { name: getDisplayNameForType('awsRDS'), onClick: goToAwsRDS, + 'data-test-subj': 'goToAWS-RDS', }, { name: getDisplayNameForType('awsSQS'), onClick: goToAwsSQS, + 'data-test-subj': 'goToAWS-SQS', }, ], }, diff --git a/x-pack/plugins/infra/public/services/telemetry/telemetry_client.mock.ts b/x-pack/plugins/infra/public/services/telemetry/telemetry_client.mock.ts index 3f913bd8d5611..604fdcc272493 100644 --- a/x-pack/plugins/infra/public/services/telemetry/telemetry_client.mock.ts +++ b/x-pack/plugins/infra/public/services/telemetry/telemetry_client.mock.ts @@ -13,4 +13,5 @@ export const createTelemetryClientMock = (): jest.Mocked => ({ reportHostFlyoutFilterRemoved: jest.fn(), reportHostFlyoutFilterAdded: jest.fn(), reportHostsViewTotalHostCountRetrieved: jest.fn(), + reportAssetDetailsFlyoutViewed: jest.fn(), }); diff --git a/x-pack/plugins/infra/public/services/telemetry/telemetry_client.ts b/x-pack/plugins/infra/public/services/telemetry/telemetry_client.ts index 56eb8d1af2c77..9107157c9835d 100644 --- a/x-pack/plugins/infra/public/services/telemetry/telemetry_client.ts +++ b/x-pack/plugins/infra/public/services/telemetry/telemetry_client.ts @@ -7,6 +7,7 @@ import { AnalyticsServiceSetup } from '@kbn/core-analytics-server'; import { + AssetDetailsFlyoutViewedParams, HostEntryClickedParams, HostFlyoutFilterActionParams, HostsViewQueryHostsCountRetrievedParams, @@ -60,4 +61,8 @@ export class TelemetryClient implements ITelemetryClient { params ); } + + public reportAssetDetailsFlyoutViewed = (params: AssetDetailsFlyoutViewedParams) => { + this.analytics.reportEvent(InfraTelemetryEventTypes.ASSET_DETAILS_FLYOUT_VIEWED, params); + }; } diff --git a/x-pack/plugins/infra/public/services/telemetry/telemetry_events.ts b/x-pack/plugins/infra/public/services/telemetry/telemetry_events.ts index 4d55baf61674b..56cce313ec219 100644 --- a/x-pack/plugins/infra/public/services/telemetry/telemetry_events.ts +++ b/x-pack/plugins/infra/public/services/telemetry/telemetry_events.ts @@ -112,7 +112,35 @@ const hostViewTotalHostCountRetrieved: InfraTelemetryEvent = { }, }; +const assetDetailsFlyoutViewed: InfraTelemetryEvent = { + eventType: InfraTelemetryEventTypes.ASSET_DETAILS_FLYOUT_VIEWED, + schema: { + componentName: { + type: 'keyword', + _meta: { + description: 'Hostname for the clicked host.', + optional: false, + }, + }, + assetType: { + type: 'keyword', + _meta: { + description: 'Cloud provider for the clicked host.', + optional: false, + }, + }, + tabId: { + type: 'keyword', + _meta: { + description: 'Cloud provider for the clicked host.', + optional: true, + }, + }, + }, +}; + export const infraTelemetryEvents = [ + assetDetailsFlyoutViewed, hostsViewQuerySubmittedEvent, hostsEntryClickedEvent, hostFlyoutRemoveFilter, diff --git a/x-pack/plugins/infra/public/services/telemetry/telemetry_service.test.ts b/x-pack/plugins/infra/public/services/telemetry/telemetry_service.test.ts index 6e2030a3fdaf3..b3c4b02468ca6 100644 --- a/x-pack/plugins/infra/public/services/telemetry/telemetry_service.test.ts +++ b/x-pack/plugins/infra/public/services/telemetry/telemetry_service.test.ts @@ -183,4 +183,28 @@ describe('TelemetryService', () => { ); }); }); + + describe('#reportAssetDetailsFlyoutViewed', () => { + it('should report asset details viewed with properties', async () => { + const setupParams = getSetupParams(); + service.setup(setupParams); + const telemetry = service.start(); + + telemetry.reportAssetDetailsFlyoutViewed({ + componentName: 'infraAssetDetailsFlyout', + assetType: 'host', + tabId: 'overview', + }); + + expect(setupParams.analytics.reportEvent).toHaveBeenCalledTimes(1); + expect(setupParams.analytics.reportEvent).toHaveBeenCalledWith( + InfraTelemetryEventTypes.ASSET_DETAILS_FLYOUT_VIEWED, + { + componentName: 'infraAssetDetailsFlyout', + assetType: 'host', + tabId: 'overview', + } + ); + }); + }); }); diff --git a/x-pack/plugins/infra/public/services/telemetry/types.ts b/x-pack/plugins/infra/public/services/telemetry/types.ts index 0ccd6c0633b4a..2ecf8115eaa58 100644 --- a/x-pack/plugins/infra/public/services/telemetry/types.ts +++ b/x-pack/plugins/infra/public/services/telemetry/types.ts @@ -18,6 +18,7 @@ export enum InfraTelemetryEventTypes { HOST_FLYOUT_FILTER_REMOVED = 'Host Flyout Filter Removed', HOST_FLYOUT_FILTER_ADDED = 'Host Flyout Filter Added', HOST_VIEW_TOTAL_HOST_COUNT_RETRIEVED = 'Host View Total Host Count Retrieved', + ASSET_DETAILS_FLYOUT_VIEWED = 'Asset Details Flyout Viewed', } export interface HostsViewQuerySubmittedParams { @@ -41,11 +42,18 @@ export interface HostsViewQueryHostsCountRetrievedParams { total: number; } +export interface AssetDetailsFlyoutViewedParams { + assetType: string; + componentName: string; + tabId?: string; +} + export type InfraTelemetryEventParams = | HostsViewQuerySubmittedParams | HostEntryClickedParams | HostFlyoutFilterActionParams - | HostsViewQueryHostsCountRetrievedParams; + | HostsViewQueryHostsCountRetrievedParams + | AssetDetailsFlyoutViewedParams; export interface ITelemetryClient { reportHostEntryClicked(params: HostEntryClickedParams): void; @@ -53,6 +61,7 @@ export interface ITelemetryClient { reportHostFlyoutFilterAdded(params: HostFlyoutFilterActionParams): void; reportHostsViewTotalHostCountRetrieved(params: HostsViewQueryHostsCountRetrievedParams): void; reportHostsViewQuerySubmitted(params: HostsViewQuerySubmittedParams): void; + reportAssetDetailsFlyoutViewed(params: AssetDetailsFlyoutViewedParams): void; } export type InfraTelemetryEvent = @@ -75,4 +84,8 @@ export type InfraTelemetryEvent = | { eventType: InfraTelemetryEventTypes.HOST_VIEW_TOTAL_HOST_COUNT_RETRIEVED; schema: RootSchema; + } + | { + eventType: InfraTelemetryEventTypes.ASSET_DETAILS_FLYOUT_VIEWED; + schema: RootSchema; }; diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout_provider.tsx b/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout_provider.tsx index b14abfe2f9895..f72fb8d00f173 100644 --- a/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout_provider.tsx +++ b/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout_provider.tsx @@ -10,15 +10,36 @@ import type { DataView } from '@kbn/data-plugin/common'; import type { FieldStatsServices } from '@kbn/unified-field-list/src/components/field_stats'; import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker'; import type { FieldStatsProps } from '@kbn/unified-field-list/src/components/field_stats'; -import { MLFieldStatsFlyoutContext } from './use_field_stats_flytout_context'; +import { useEffect } from 'react'; +import { getProcessedFields } from '@kbn/ml-data-grid'; +import { stringHash } from '@kbn/ml-string-hash'; +import { lastValueFrom } from 'rxjs'; +import { useRef } from 'react'; +import { getMergedSampleDocsForPopulatedFieldsQuery } from './populated_fields/get_merged_populated_fields_query'; +import { useMlKibana } from '../../contexts/kibana'; import { FieldStatsFlyout } from './field_stats_flyout'; +import { MLFieldStatsFlyoutContext } from './use_field_stats_flytout_context'; +import { PopulatedFieldsCacheManager } from './populated_fields/populated_fields_cache_manager'; export const FieldStatsFlyoutProvider: FC<{ dataView: DataView; fieldStatsServices: FieldStatsServices; timeRangeMs?: TimeRangeMs; dslQuery?: FieldStatsProps['dslQuery']; -}> = ({ dataView, fieldStatsServices, timeRangeMs, dslQuery, children }) => { + disablePopulatedFields?: boolean; +}> = ({ + dataView, + fieldStatsServices, + timeRangeMs, + dslQuery, + disablePopulatedFields = false, + children, +}) => { + const { + services: { + data: { search }, + }, + } = useMlKibana(); const [isFieldStatsFlyoutVisible, setFieldStatsIsFlyoutVisible] = useState(false); const [fieldName, setFieldName] = useState(); const [fieldValue, setFieldValue] = useState(); @@ -27,6 +48,86 @@ export const FieldStatsFlyoutProvider: FC<{ () => setFieldStatsIsFlyoutVisible(!isFieldStatsFlyoutVisible), [isFieldStatsFlyoutVisible] ); + const [manager] = useState(new PopulatedFieldsCacheManager()); + const [populatedFields, setPopulatedFields] = useState | undefined>(); + const abortController = useRef(new AbortController()); + + useEffect( + function fetchSampleDocsEffect() { + if (disablePopulatedFields) return; + + let unmounted = false; + + if (abortController.current) { + abortController.current.abort(); + abortController.current = new AbortController(); + } + + const queryAndRunTimeMappings = getMergedSampleDocsForPopulatedFieldsQuery({ + searchQuery: dslQuery, + runtimeFields: dataView.getRuntimeMappings(), + datetimeField: dataView.getTimeField()?.name, + timeRange: timeRangeMs, + }); + const indexPattern = dataView.getIndexPattern(); + const esSearchRequestParams = { + index: indexPattern, + body: { + fields: ['*'], + _source: false, + ...queryAndRunTimeMappings, + size: 1000, + }, + }; + const cacheKey = stringHash(JSON.stringify(esSearchRequestParams)).toString(); + + const fetchSampleDocuments = async function () { + try { + const resp = await lastValueFrom( + search.search( + { + params: esSearchRequestParams, + }, + { abortSignal: abortController.current.signal } + ) + ); + + const docs = resp.rawResponse.hits.hits.map((d) => getProcessedFields(d.fields ?? {})); + + // Get all field names for each returned doc and flatten it + // to a list of unique field names used across all docs. + const fieldsWithData = new Set(docs.map(Object.keys).flat(1)); + manager.set(cacheKey, fieldsWithData); + if (!unmounted) { + setPopulatedFields(fieldsWithData); + } + } catch (e) { + if (e.name !== 'AbortError') { + // eslint-disable-next-line no-console + console.error( + `An error occurred fetching sample documents to determine populated field stats. + \nQuery:\n${JSON.stringify(esSearchRequestParams)} + \nError:${e}` + ); + } + } + }; + + const cachedResult = manager.get(cacheKey); + if (cachedResult) { + return cachedResult; + } else { + fetchSampleDocuments(); + } + + return () => { + unmounted = true; + abortController.current.abort(); + }; + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [JSON.stringify({ dslQuery, dataViewId: dataView.id, timeRangeMs })] + ); return ( ; export const FieldStatsInfoButton = ({ field, label, - searchValue = '', onButtonClick, + disabled, + isEmpty = false, + hideTrigger = false, }: { field: FieldForStats; label: string; searchValue?: string; + disabled?: boolean; + isEmpty?: boolean; onButtonClick?: (field: FieldForStats) => void; + hideTrigger?: boolean; }) => { + const themeVars = useCurrentThemeVars(); + const emptyFieldMessage = isEmpty + ? ' ' + + i18n.translate('xpack.ml.newJob.wizard.fieldContextPopover.emptyFieldInSampleDocsMsg', { + defaultMessage: '(no data found in 1000 sample records)', + }) + : ''; return ( - - ) => { - if (ev.type === 'click') { - ev.currentTarget.focus(); - } - ev.preventDefault(); - ev.stopPropagation(); + > + ) => { + if (ev.type === 'click') { + ev.currentTarget.focus(); + } + ev.preventDefault(); + ev.stopPropagation(); - if (onButtonClick) { - onButtonClick(field); - } - }} - aria-label={i18n.translate( - 'xpack.ml.newJob.wizard.fieldContextPopover.inspectFieldStatsTooltipArialabel', - { - defaultMessage: 'Inspect field statistics', + if (onButtonClick) { + onButtonClick(field); + } + }} + aria-label={ + i18n.translate( + 'xpack.ml.newJob.wizard.fieldContextPopover.inspectFieldStatsTooltipAriaLabel', + { + defaultMessage: 'Inspect field statistics', + } + ) + emptyFieldMessage } - )} - /> - + /> + + ) : null} - - + + - {label} + + {label} + ); diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/get_merged_populated_fields_query.test.ts b/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/get_merged_populated_fields_query.test.ts new file mode 100644 index 0000000000000..0ead649ab428a --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/get_merged_populated_fields_query.test.ts @@ -0,0 +1,106 @@ +/* + * 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 { getMergedSampleDocsForPopulatedFieldsQuery } from './get_merged_populated_fields_query'; + +describe('getMergedSampleDocsForPopulatedFieldsQuery()', () => { + it('should wrap the original query in function_score', () => { + expect( + getMergedSampleDocsForPopulatedFieldsQuery({ + searchQuery: { match_all: {} }, + runtimeFields: {}, + }) + ).toStrictEqual({ + query: { + function_score: { query: { bool: { must: [{ match_all: {} }] } }, random_score: {} }, + }, + runtime_mappings: {}, + }); + }); + + it('should append the time range to the query if timeRange and datetimeField are provided', () => { + expect( + getMergedSampleDocsForPopulatedFieldsQuery({ + searchQuery: { + bool: { + should: [{ match_phrase: { version: '1' } }], + minimum_should_match: 1, + filter: [ + { + terms: { + cluster_uuid: '', + }, + }, + ], + must_not: [], + }, + }, + runtimeFields: {}, + timeRange: { from: 1613995874349, to: 1614082617000 }, + datetimeField: '@timestamp', + }) + ).toStrictEqual({ + query: { + function_score: { + query: { + bool: { + filter: [ + { terms: { cluster_uuid: '' } }, + { + range: { + '@timestamp': { + format: 'epoch_millis', + gte: 1613995874349, + lte: 1614082617000, + }, + }, + }, + ], + minimum_should_match: 1, + must_not: [], + should: [{ match_phrase: { version: '1' } }], + }, + }, + random_score: {}, + }, + }, + runtime_mappings: {}, + }); + }); + + it('should not append the time range to the query if datetimeField is undefined', () => { + expect( + getMergedSampleDocsForPopulatedFieldsQuery({ + searchQuery: { + bool: { + should: [{ match_phrase: { airline: 'AAL' } }], + minimum_should_match: 1, + filter: [], + must_not: [], + }, + }, + runtimeFields: {}, + timeRange: { from: 1613995874349, to: 1614082617000 }, + }) + ).toStrictEqual({ + query: { + function_score: { + query: { + bool: { + filter: [], + minimum_should_match: 1, + must_not: [], + should: [{ match_phrase: { airline: 'AAL' } }], + }, + }, + random_score: {}, + }, + }, + runtime_mappings: {}, + }); + }); +}); diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/get_merged_populated_fields_query.ts b/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/get_merged_populated_fields_query.ts new file mode 100644 index 0000000000000..7287392849980 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/get_merged_populated_fields_query.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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import { cloneDeep } from 'lodash'; +import { getDefaultDSLQuery } from '@kbn/ml-query-utils'; + +export const getMergedSampleDocsForPopulatedFieldsQuery = ({ + runtimeFields, + searchQuery, + datetimeField, + timeRange, +}: { + runtimeFields: estypes.MappingRuntimeFields; + searchQuery?: estypes.QueryDslQueryContainer; + datetimeField?: string; + timeRange?: TimeRangeMs; +}): { + query: estypes.QueryDslQueryContainer; + runtime_mappings?: estypes.MappingRuntimeFields; +} => { + let rangeFilter; + + if (timeRange && datetimeField !== undefined) { + if (isPopulatedObject(timeRange, ['from', 'to']) && timeRange.to > timeRange.from) { + rangeFilter = { + range: { + [datetimeField]: { + gte: timeRange.from, + lte: timeRange.to, + format: 'epoch_millis', + }, + }, + }; + } + } + + const query = cloneDeep( + !searchQuery || isPopulatedObject(searchQuery, ['match_all']) + ? getDefaultDSLQuery() + : searchQuery + ); + + if (rangeFilter && isPopulatedObject(query, ['bool'])) { + if (Array.isArray(query.bool.filter)) { + query.bool.filter.push(rangeFilter); + } else { + query.bool.filter = [rangeFilter]; + } + } + + const queryAndRuntimeFields: { + query: estypes.QueryDslQueryContainer; + runtime_mappings?: estypes.MappingRuntimeFields; + } = { + query: { + function_score: { + query, + // @ts-expect-error random_score is valid dsl query + random_score: {}, + }, + }, + }; + if (runtimeFields) { + queryAndRuntimeFields.runtime_mappings = runtimeFields; + } + return queryAndRuntimeFields; +}; diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/index.ts b/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/index.ts new file mode 100644 index 0000000000000..339f112beeb9f --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { PopulatedFieldsCacheManager } from './populated_fields_cache_manager'; diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/populated_fields_cache_manager.ts b/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/populated_fields_cache_manager.ts new file mode 100644 index 0000000000000..547ad65c1179e --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/populated_fields_cache_manager.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +type StringifiedQueryKey = string; +type UpdatedTimestamp = number; + +const DEFAULT_EXPIRATION_MS = 60000; +export class PopulatedFieldsCacheManager { + private _resultsCache = new Map(); + private readonly _lastUpdatedTimestamps = new Map(); + + constructor(private readonly _expirationDurationMs = DEFAULT_EXPIRATION_MS) {} + + private clearOldCacheIfNeeded() { + if (this._resultsCache.size > 10) { + this._resultsCache.clear(); + this._lastUpdatedTimestamps.clear(); + } + } + + private clearExpiredCache(key: StringifiedQueryKey) { + // If result is available but past the expiration duration, clear cache + const lastUpdatedTs = this._lastUpdatedTimestamps.get(key); + const now = Date.now(); + if (lastUpdatedTs !== undefined && lastUpdatedTs - now > this._expirationDurationMs) { + this._resultsCache.delete(key); + } + } + + public get(key: StringifiedQueryKey) { + return this._resultsCache.get(key); + } + + public set(key: StringifiedQueryKey, value: any) { + this.clearExpiredCache(key); + this.clearOldCacheIfNeeded(); + + this._resultsCache.set(key, Date.now()); + this._resultsCache.set(key, value); + } +} diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/use_field_stats_flytout_context.ts b/x-pack/plugins/ml/public/application/components/field_stats_flyout/use_field_stats_flytout_context.ts index 2de9fda1ded17..8aa7cfcc42de4 100644 --- a/x-pack/plugins/ml/public/application/components/field_stats_flyout/use_field_stats_flytout_context.ts +++ b/x-pack/plugins/ml/public/application/components/field_stats_flyout/use_field_stats_flytout_context.ts @@ -6,6 +6,7 @@ */ import { createContext, useContext } from 'react'; +import { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker'; interface MLJobWizardFieldStatsFlyoutProps { isFlyoutVisible: boolean; setIsFlyoutVisible: (v: boolean) => void; @@ -14,6 +15,8 @@ interface MLJobWizardFieldStatsFlyoutProps { fieldName?: string; setFieldValue: (v: string) => void; fieldValue?: string | number; + timeRangeMs?: TimeRangeMs; + populatedFields?: Set; } export const MLFieldStatsFlyoutContext = createContext({ isFlyoutVisible: false, @@ -21,6 +24,8 @@ export const MLFieldStatsFlyoutContext = createContext {}, setFieldName: () => {}, setFieldValue: () => {}, + timeRangeMs: undefined, + populatedFields: undefined, }); export function useFieldStatsFlyoutContext() { diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/use_field_stats_trigger.tsx b/x-pack/plugins/ml/public/application/components/field_stats_flyout/use_field_stats_trigger.tsx index da69ad87c46bc..52011e72a6ddc 100644 --- a/x-pack/plugins/ml/public/application/components/field_stats_flyout/use_field_stats_trigger.tsx +++ b/x-pack/plugins/ml/public/application/components/field_stats_flyout/use_field_stats_trigger.tsx @@ -6,17 +6,17 @@ */ import React, { ReactNode, useCallback } from 'react'; -import { EuiComboBoxOptionOption } from '@elastic/eui'; +import type { EuiComboBoxOptionOption } from '@elastic/eui'; import type { Field } from '@kbn/ml-anomaly-utils'; import { optionCss } from './eui_combo_box_with_field_stats'; import { useFieldStatsFlyoutContext } from '.'; import { FieldForStats, FieldStatsInfoButton } from './field_stats_info_button'; - interface Option extends EuiComboBoxOptionOption { field: Field; } + export const useFieldStatsTrigger = () => { - const { setIsFlyoutVisible, setFieldName } = useFieldStatsFlyoutContext(); + const { setIsFlyoutVisible, setFieldName, populatedFields } = useFieldStatsFlyoutContext(); const closeFlyout = useCallback(() => setIsFlyoutVisible(false), [setIsFlyoutVisible]); @@ -29,6 +29,7 @@ export const useFieldStatsTrigger = () => { }, [setFieldName, setIsFlyoutVisible] ); + const renderOption = useCallback( (option: EuiComboBoxOptionOption, searchValue: string): ReactNode => { const field = (option as Option).field; @@ -36,13 +37,15 @@ export const useFieldStatsTrigger = () => { option.label ) : ( ); }, - [handleFieldStatsButtonClick] + // eslint-disable-next-line react-hooks/exhaustive-deps + [handleFieldStatsButtonClick, populatedFields?.size] ); return { renderOption, @@ -51,5 +54,6 @@ export const useFieldStatsTrigger = () => { handleFieldStatsButtonClick, closeFlyout, optionCss, + populatedFields, }; }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index 447d8fbd45aba..5a3f26a63975e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -376,7 +376,7 @@ export const validateAdvancedEditor = (state: State): State => { !resultsFieldEmptyString && !dependentVariableEmpty && !modelMemoryLimitEmpty && - numTopFeatureImportanceValuesValid && + (numTopFeatureImportanceValuesValid || jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION) && (!destinationIndexPatternTitleExists || !createIndexPattern); return state; @@ -457,7 +457,7 @@ const validateForm = (state: State): State => { !destinationIndexNameEmpty && destinationIndexNameValid && !dependentVariableEmpty && - numTopFeatureImportanceValuesValid && + (numTopFeatureImportanceValuesValid || jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION) && (!destinationIndexPatternTitleExists || !createIndexPattern); return state; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx index 4c8d2996a318b..824fb52398fd4 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx @@ -8,6 +8,7 @@ import React, { FC, useContext, useState, useEffect, useMemo } from 'react'; import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; import type { Field, Aggregation, AggFieldPair } from '@kbn/ml-anomaly-utils'; +import { EVENT_RATE_FIELD_ID } from '@kbn/ml-anomaly-utils'; import { FieldStatsInfoButton } from '../../../../../../../components/field_stats_flyout/field_stats_info_button'; import { JobCreatorContext } from '../../../job_creator_context'; import { useFieldStatsTrigger } from '../../../../../../../components/field_stats_flyout/use_field_stats_trigger'; @@ -43,7 +44,7 @@ export const AggSelect: FC = ({ fields, changeHandler, selectedOptions, r // create list of labels based on already selected detectors // so they can be removed from the dropdown list const removeLabels = removeOptions.map(createLabel); - const { handleFieldStatsButtonClick } = useFieldStatsTrigger(); + const { handleFieldStatsButtonClick, populatedFields } = useFieldStatsTrigger(); const options: EuiComboBoxOptionOption[] = useMemo( () => @@ -55,6 +56,8 @@ export const AggSelect: FC = ({ fields, changeHandler, selectedOptions, r // for more robust rendering label: ( = ({ fields, changeHandler, selectedOptions, r } return aggOption; }), - [handleFieldStatsButtonClick, fields, removeLabels] + // eslint-disable-next-line react-hooks/exhaustive-deps + [handleFieldStatsButtonClick, fields, removeLabels, populatedFields?.size] ); useEffect(() => { diff --git a/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/allocated_models.tsx b/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/allocated_models.tsx index 5cdc922824ae7..02738f3bebe51 100644 --- a/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/allocated_models.tsx +++ b/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/allocated_models.tsx @@ -122,13 +122,27 @@ export const AllocatedModels: FC = ({ }, }, { - field: 'node.throughput_last_minute', - name: i18n.translate( - 'xpack.ml.trainedModels.nodesList.modelsList.throughputLastMinuteHeader', - { - defaultMessage: 'Throughput', - } + name: ( + + + {i18n.translate( + 'xpack.ml.trainedModels.nodesList.modelsList.throughputLastMinuteHeader', + { + defaultMessage: 'Throughput', + } + )} + + + ), + field: 'node.throughput_last_minute', width: '100px', truncateText: false, 'data-test-subj': 'mlAllocatedModelsTableThroughput', diff --git a/x-pack/plugins/observability/common/field_names/slo.ts b/x-pack/plugins/observability/common/field_names/slo.ts index 24e94f5db91c8..2b93accf03d8e 100644 --- a/x-pack/plugins/observability/common/field_names/slo.ts +++ b/x-pack/plugins/observability/common/field_names/slo.ts @@ -7,3 +7,4 @@ export const SLO_ID_FIELD = 'slo.id'; export const SLO_REVISION_FIELD = 'slo.revision'; +export const SLO_INSTANCE_ID_FIELD = 'slo.instanceId'; diff --git a/x-pack/plugins/observability/docs/openapi/slo/bundled.json b/x-pack/plugins/observability/docs/openapi/slo/bundled.json index efc5fd9c5b15f..a949bc868850e 100644 --- a/x-pack/plugins/observability/docs/openapi/slo/bundled.json +++ b/x-pack/plugins/observability/docs/openapi/slo/bundled.json @@ -1264,6 +1264,12 @@ "error_budget": { "title": "Error budget", "type": "object", + "required": [ + "initial", + "consumed", + "remaining", + "isEstimated" + ], "properties": { "initial": { "type": "number", @@ -1291,6 +1297,11 @@ "title": "Summary", "type": "object", "description": "The SLO computed data", + "required": [ + "status", + "sliValue", + "errorBudget" + ], "properties": { "status": { "$ref": "#/components/schemas/summary_status" @@ -1307,6 +1318,23 @@ "slo_response": { "title": "SLO response", "type": "object", + "required": [ + "id", + "name", + "description", + "indicator", + "timeWindow", + "budgetingMethod", + "objective", + "settings", + "revision", + "summary", + "enabled", + "groupBy", + "instanceId", + "createdAt", + "updatedAt" + ], "properties": { "id": { "description": "The identifier of the SLO.", diff --git a/x-pack/plugins/observability/docs/openapi/slo/bundled.yaml b/x-pack/plugins/observability/docs/openapi/slo/bundled.yaml index ec2aa41bc77af..1a134dd4d5d6b 100644 --- a/x-pack/plugins/observability/docs/openapi/slo/bundled.yaml +++ b/x-pack/plugins/observability/docs/openapi/slo/bundled.yaml @@ -867,6 +867,11 @@ components: error_budget: title: Error budget type: object + required: + - initial + - consumed + - remaining + - isEstimated properties: initial: type: number @@ -888,6 +893,10 @@ components: title: Summary type: object description: The SLO computed data + required: + - status + - sliValue + - errorBudget properties: status: $ref: '#/components/schemas/summary_status' @@ -899,6 +908,22 @@ components: slo_response: title: SLO response type: object + required: + - id + - name + - description + - indicator + - timeWindow + - budgetingMethod + - objective + - settings + - revision + - summary + - enabled + - groupBy + - instanceId + - createdAt + - updatedAt properties: id: description: The identifier of the SLO. diff --git a/x-pack/plugins/observability/docs/openapi/slo/components/schemas/error_budget.yaml b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/error_budget.yaml index c344c8472821d..3fc9505989fe9 100644 --- a/x-pack/plugins/observability/docs/openapi/slo/components/schemas/error_budget.yaml +++ b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/error_budget.yaml @@ -1,5 +1,10 @@ title: Error budget type: object +required: + - initial + - consumed + - remaining + - isEstimated properties: initial: type: number diff --git a/x-pack/plugins/observability/docs/openapi/slo/components/schemas/slo_response.yaml b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/slo_response.yaml index 0663c31e40a0f..b4e5eb85f3264 100644 --- a/x-pack/plugins/observability/docs/openapi/slo/components/schemas/slo_response.yaml +++ b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/slo_response.yaml @@ -1,5 +1,21 @@ title: SLO response type: object +required: + - id + - name + - description + - indicator + - timeWindow + - budgetingMethod + - objective + - settings + - revision + - summary + - enabled + - groupBy + - instanceId + - createdAt + - updatedAt properties: id: description: The identifier of the SLO. diff --git a/x-pack/plugins/observability/docs/openapi/slo/components/schemas/summary.yaml b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/summary.yaml index a7b1be7d053b1..5dfb724f31834 100644 --- a/x-pack/plugins/observability/docs/openapi/slo/components/schemas/summary.yaml +++ b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/summary.yaml @@ -1,6 +1,10 @@ title: Summary type: object description: The SLO computed data +required: + - status + - sliValue + - errorBudget properties: status: $ref: './summary_status.yaml' diff --git a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/burn_rate_rule_editor.tsx b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/burn_rate_rule_editor.tsx index 5f38c9509117a..7e0e88d6c148d 100644 --- a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/burn_rate_rule_editor.tsx +++ b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/burn_rate_rule_editor.tsx @@ -7,9 +7,10 @@ import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public'; import React, { useEffect, useState } from 'react'; -import { SLOResponse } from '@kbn/slo-schema'; +import { ALL_VALUE, SLOResponse } from '@kbn/slo-schema'; -import { EuiSpacer, EuiTitle } from '@elastic/eui'; +import { EuiCallOut, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { useFetchSloDetails } from '../../hooks/slo/use_fetch_slo_details'; import { BurnRateRuleParams, WindowSchema } from '../../typings'; import { SloSelector } from './slo_selector'; @@ -98,6 +99,20 @@ export function BurnRateRuleEditor(props: Props) { + {selectedSlo?.groupBy && selectedSlo.groupBy !== ALL_VALUE && ( + <> + + + + )}
Define multiple burn rate windows
diff --git a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/slo_selector.test.tsx b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/slo_selector.test.tsx index 12d837fafaba2..ce401e7e0f1c2 100644 --- a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/slo_selector.test.tsx +++ b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/slo_selector.test.tsx @@ -11,26 +11,26 @@ import { wait } from '@testing-library/user-event/dist/utils'; import React from 'react'; import { emptySloList } from '../../data/slo/slo'; -import { useFetchSloList } from '../../hooks/slo/use_fetch_slo_list'; +import { useFetchSloDefinitions } from '../../hooks/slo/use_fetch_slo_definitions'; import { render } from '../../utils/test_helper'; import { SloSelector } from './slo_selector'; -jest.mock('../../hooks/slo/use_fetch_slo_list'); +jest.mock('../../hooks/slo/use_fetch_slo_definitions'); -const useFetchSloListMock = useFetchSloList as jest.Mock; +const useFetchSloDefinitionsMock = useFetchSloDefinitions as jest.Mock; describe('SLO Selector', () => { const onSelectedSpy = jest.fn(); beforeEach(() => { jest.clearAllMocks(); - useFetchSloListMock.mockReturnValue({ isLoading: true, sloList: emptySloList }); + useFetchSloDefinitionsMock.mockReturnValue({ isLoading: true, data: emptySloList }); }); it('fetches SLOs asynchronously', async () => { render(); expect(screen.getByTestId('sloSelector')).toBeTruthy(); - expect(useFetchSloListMock).toHaveBeenCalledWith({ kqlQuery: 'slo.name:*' }); + expect(useFetchSloDefinitionsMock).toHaveBeenCalledWith({ name: '' }); }); it('searches SLOs when typing', async () => { @@ -42,6 +42,6 @@ describe('SLO Selector', () => { await wait(310); // debounce delay }); - expect(useFetchSloListMock).toHaveBeenCalledWith({ kqlQuery: 'slo.name:latency*' }); + expect(useFetchSloDefinitionsMock).toHaveBeenCalledWith({ name: 'latency' }); }); }); diff --git a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/slo_selector.tsx b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/slo_selector.tsx index 4dd13c92fcfa3..5ec21da2efc1c 100644 --- a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/slo_selector.tsx +++ b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/slo_selector.tsx @@ -10,8 +10,7 @@ import { i18n } from '@kbn/i18n'; import { SLOResponse } from '@kbn/slo-schema'; import { debounce } from 'lodash'; import React, { useEffect, useMemo, useState } from 'react'; - -import { useFetchSloList } from '../../hooks/slo/use_fetch_slo_list'; +import { useFetchSloDefinitions } from '../../hooks/slo/use_fetch_slo_definitions'; interface Props { initialSlo?: SLOResponse; @@ -23,7 +22,7 @@ function SloSelector({ initialSlo, onSelected, errors }: Props) { const [options, setOptions] = useState>>([]); const [selectedOptions, setSelectedOptions] = useState>>(); const [searchValue, setSearchValue] = useState(''); - const { isLoading, sloList } = useFetchSloList({ kqlQuery: `slo.name:${searchValue}*` }); + const { isLoading, data: sloList } = useFetchSloDefinitions({ name: searchValue }); const hasError = errors !== undefined && errors.length > 0; useEffect(() => { @@ -33,7 +32,7 @@ function SloSelector({ initialSlo, onSelected, errors }: Props) { useEffect(() => { const isLoadedWithData = !isLoading && sloList !== undefined; const opts: Array> = isLoadedWithData - ? sloList.results.map((slo) => ({ value: slo.id, label: slo.name })) + ? sloList.map((slo) => ({ value: slo.id, label: slo.name })) : []; setOptions(opts); }, [isLoading, sloList]); @@ -41,7 +40,7 @@ function SloSelector({ initialSlo, onSelected, errors }: Props) { const onChange = (opts: Array>) => { setSelectedOptions(opts); const selectedSlo = - opts.length === 1 ? sloList?.results.find((slo) => slo.id === opts[0].value) : undefined; + opts.length === 1 ? sloList?.find((slo) => slo.id === opts[0].value) : undefined; onSelected(selectedSlo); }; diff --git a/x-pack/plugins/observability/public/components/slo/slo_status_badge/slo_active_alerts_badge.stories.tsx b/x-pack/plugins/observability/public/components/slo/slo_status_badge/slo_active_alerts_badge.stories.tsx index 3aed4658ab766..c9f07555897c5 100644 --- a/x-pack/plugins/observability/public/components/slo/slo_status_badge/slo_active_alerts_badge.stories.tsx +++ b/x-pack/plugins/observability/public/components/slo/slo_status_badge/slo_active_alerts_badge.stories.tsx @@ -25,4 +25,4 @@ const Template: ComponentStory = (props: Props) => ( ); export const Default = Template.bind({}); -Default.args = { activeAlerts: { count: 2 } }; +Default.args = { activeAlerts: 2 }; diff --git a/x-pack/plugins/observability/public/components/slo/slo_status_badge/slo_active_alerts_badge.tsx b/x-pack/plugins/observability/public/components/slo/slo_status_badge/slo_active_alerts_badge.tsx index a150c5aa2d88a..485355a278721 100644 --- a/x-pack/plugins/observability/public/components/slo/slo_status_badge/slo_active_alerts_badge.tsx +++ b/x-pack/plugins/observability/public/components/slo/slo_status_badge/slo_active_alerts_badge.tsx @@ -12,10 +12,9 @@ import { SLOWithSummaryResponse } from '@kbn/slo-schema'; import { paths } from '../../../../common/locators/paths'; import { useKibana } from '../../../utils/kibana_react'; -import { ActiveAlerts } from '../../../hooks/slo/use_fetch_active_alerts'; export interface Props { - activeAlerts?: ActiveAlerts; + activeAlerts?: number; slo: SLOWithSummaryResponse; } @@ -53,7 +52,7 @@ export function SloActiveAlertsBadge({ slo, activeAlerts }: Props) { > {i18n.translate('xpack.observability.slo.slo.activeAlertsBadge.label', { defaultMessage: '{count, plural, one {# alert} other {# alerts}}', - values: { count: activeAlerts.count }, + values: { count: activeAlerts }, })}
diff --git a/x-pack/plugins/observability/public/hooks/slo/__storybook_mocks__/use_fetch_active_alerts.ts b/x-pack/plugins/observability/public/hooks/slo/__storybook_mocks__/use_fetch_active_alerts.ts index f493eadac3806..5eaee397b9ab7 100644 --- a/x-pack/plugins/observability/public/hooks/slo/__storybook_mocks__/use_fetch_active_alerts.ts +++ b/x-pack/plugins/observability/public/hooks/slo/__storybook_mocks__/use_fetch_active_alerts.ts @@ -5,23 +5,24 @@ * 2.0. */ -import { UseFetchActiveAlerts } from '../use_fetch_active_alerts'; +import { ActiveAlerts, UseFetchActiveAlerts } from '../use_fetch_active_alerts'; export const useFetchActiveAlerts = ({ - sloIds = [], + sloIdsAndInstanceIds = [], }: { - sloIds: string[]; + sloIdsAndInstanceIds: Array<[string, string]>; }): UseFetchActiveAlerts => { + const data = sloIdsAndInstanceIds.reduce( + (acc, item, index) => ({ + ...acc, + ...(index % 2 === 0 && { [item.join('|')]: 2 }), + }), + {} + ); return { isLoading: false, isSuccess: false, isError: false, - data: sloIds.reduce( - (acc, sloId, index) => ({ - ...acc, - ...(index % 2 === 0 && { [sloId]: { count: 2, ruleIds: ['rule-1', 'rule-2'] } }), - }), - {} - ), + data: new ActiveAlerts(data), }; }; diff --git a/x-pack/plugins/observability/public/hooks/slo/query_key_factory.ts b/x-pack/plugins/observability/public/hooks/slo/query_key_factory.ts index bf4a748ddfe58..74e2e31be4151 100644 --- a/x-pack/plugins/observability/public/hooks/slo/query_key_factory.ts +++ b/x-pack/plugins/observability/public/hooks/slo/query_key_factory.ts @@ -29,7 +29,8 @@ export const sloKeys = { rules: () => [...sloKeys.all, 'rules'] as const, rule: (sloIds: string[]) => [...sloKeys.rules(), sloIds] as const, activeAlerts: () => [...sloKeys.all, 'activeAlerts'] as const, - activeAlert: (sloIds: string[]) => [...sloKeys.activeAlerts(), sloIds] as const, + activeAlert: (sloIdsAndInstanceIds: Array<[string, string]>) => + [...sloKeys.activeAlerts(), ...sloIdsAndInstanceIds.flat()] as const, historicalSummaries: () => [...sloKeys.all, 'historicalSummary'] as const, historicalSummary: (list: Array<{ sloId: string; instanceId: string }>) => [...sloKeys.historicalSummaries(), list] as const, diff --git a/x-pack/plugins/observability/public/hooks/slo/use_fetch_active_alerts.ts b/x-pack/plugins/observability/public/hooks/slo/use_fetch_active_alerts.ts index 580d72e550e6b..601fc8f024a89 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_fetch_active_alerts.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_fetch_active_alerts.ts @@ -8,23 +8,50 @@ import { useQuery } from '@tanstack/react-query'; import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common'; +import { ALL_VALUE, SLOResponse } from '@kbn/slo-schema'; import { useKibana } from '../../utils/kibana_react'; import { sloKeys } from './query_key_factory'; -type SloId = string; +type SLO = Pick; -interface Params { - sloIds: SloId[]; -} +export class ActiveAlerts { + private data: Map = new Map(); + + constructor(initialData?: Record) { + if (initialData) { + Object.keys(initialData).forEach((key) => this.data.set(key, initialData[key])); + } + } + + set(slo: SLO, value: number) { + this.data.set(`${slo.id}|${slo.instanceId ?? ALL_VALUE}`, value); + } + + get(slo: SLO) { + return this.data.get(`${slo.id}|${slo.instanceId ?? ALL_VALUE}`); + } + + has(slo: SLO) { + return this.data.has(`${slo.id}|${slo.instanceId ?? ALL_VALUE}`); + } + + delete(slo: SLO) { + return this.data.delete(`${slo.id}|${slo.instanceId ?? ALL_VALUE}`); + } -export interface ActiveAlerts { - count: number; + clear() { + return this.data.clear(); + } } -type ActiveAlertsMap = Record; +type SloIdAndInstanceId = [string, string]; + +interface Params { + sloIdsAndInstanceIds: SloIdAndInstanceId[]; +} export interface UseFetchActiveAlerts { - data: ActiveAlertsMap; + data: ActiveAlerts; isLoading: boolean; isSuccess: boolean; isError: boolean; @@ -34,20 +61,21 @@ interface FindApiResponse { aggregations: { perSloId: { buckets: Array<{ - key: string; + key: SloIdAndInstanceId; + key_as_string: string; doc_count: number; }>; }; }; } -const EMPTY_ACTIVE_ALERTS_MAP = {}; +const EMPTY_ACTIVE_ALERTS_MAP = new ActiveAlerts(); -export function useFetchActiveAlerts({ sloIds = [] }: Params): UseFetchActiveAlerts { +export function useFetchActiveAlerts({ sloIdsAndInstanceIds = [] }: Params): UseFetchActiveAlerts { const { http } = useKibana().services; const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({ - queryKey: sloKeys.activeAlert(sloIds), + queryKey: sloKeys.activeAlert(sloIdsAndInstanceIds), queryFn: async ({ signal }) => { try { const response = await http.post(`${BASE_RAC_ALERTS_API_PATH}/find`, { @@ -75,21 +103,22 @@ export function useFetchActiveAlerts({ sloIds = [] }: Params): UseFetchActiveAle }, }, ], - should: [ - { - terms: { - 'kibana.alert.rule.parameters.sloId': sloIds, - }, + should: sloIdsAndInstanceIds.map(([sloId, instanceId]) => ({ + bool: { + filter: [ + { term: { 'slo.id': sloId } }, + { term: { 'slo.instanceId': instanceId } }, + ], }, - ], + })), minimum_should_match: 1, }, }, aggs: { perSloId: { - terms: { - size: sloIds.length, - field: 'kibana.alert.rule.parameters.sloId', + multi_terms: { + size: sloIdsAndInstanceIds.length, + terms: [{ field: 'slo.id' }, { field: 'slo.instanceId' }], }, }, }, @@ -97,15 +126,10 @@ export function useFetchActiveAlerts({ sloIds = [] }: Params): UseFetchActiveAle signal, }); - return response.aggregations.perSloId.buckets.reduce( - (acc, bucket) => ({ - ...acc, - [bucket.key]: { - count: bucket.doc_count ?? 0, - } as ActiveAlerts, - }), - {} - ); + const activeAlertsData = response.aggregations.perSloId.buckets.reduce((acc, bucket) => { + return { ...acc, [bucket.key_as_string]: bucket.doc_count ?? 0 }; + }, {} as Record); + return new ActiveAlerts(activeAlertsData); } catch (error) { // ignore error } diff --git a/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_definitions.ts b/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_definitions.ts new file mode 100644 index 0000000000000..33a480254c826 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_definitions.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + QueryObserverResult, + RefetchOptions, + RefetchQueryFilters, + useQuery, +} from '@tanstack/react-query'; +import { SLOResponse } from '@kbn/slo-schema'; +import { useKibana } from '../../utils/kibana_react'; + +export interface UseFetchSloDefinitionsResponse { + isLoading: boolean; + isSuccess: boolean; + isError: boolean; + data: SLOResponse[] | undefined; + refetch: ( + options?: (RefetchOptions & RefetchQueryFilters) | undefined + ) => Promise>; +} + +interface Params { + name?: string; + size?: number; +} + +export function useFetchSloDefinitions({ + name = '', + size = 10, +}: Params): UseFetchSloDefinitionsResponse { + const { savedObjects } = useKibana().services; + const search = name.endsWith('*') ? name : `${name}*`; + + const { isLoading, isError, isSuccess, data, refetch } = useQuery({ + queryKey: ['fetchSloDefinitions', search], + queryFn: async () => { + try { + const response = await savedObjects.client.find({ + type: 'slo', + search, + searchFields: ['name'], + perPage: size, + }); + return response.savedObjects.map((so) => so.attributes); + } catch (error) { + throw new Error(`Something went wrong. Error: ${error}`); + } + }, + }); + + return { isLoading, isError, isSuccess, data, refetch }; +} diff --git a/x-pack/plugins/observability/public/pages/alert_details/alert_details.test.tsx b/x-pack/plugins/observability/public/pages/alert_details/alert_details.test.tsx index 4cc8895ae6a4c..a5bb4cbb4aeb5 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/alert_details.test.tsx +++ b/x-pack/plugins/observability/public/pages/alert_details/alert_details.test.tsx @@ -49,6 +49,7 @@ const mockKibana = () => { useKibanaMock.mockReturnValue({ services: { ...kibanaStartMock.startContract(), + theme: {}, cases: casesPluginMock.createStartContract(), http: { basePath: { diff --git a/x-pack/plugins/observability/public/pages/alerts/components/alert_actions.tsx b/x-pack/plugins/observability/public/pages/alerts/components/alert_actions.tsx index b82b45f1065e2..14afd4994b2be 100644 --- a/x-pack/plugins/observability/public/pages/alerts/components/alert_actions.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/alert_actions.tsx @@ -248,7 +248,11 @@ export function AlertActions({ isOpen={isPopoverOpen} panelPaddingSize="none" > - +
diff --git a/x-pack/plugins/observability/public/pages/overview/components/header_menu/header_menu.tsx b/x-pack/plugins/observability/public/pages/overview/components/header_menu/header_menu.tsx index 7b1234ecaacea..111acb054e163 100644 --- a/x-pack/plugins/observability/public/pages/overview/components/header_menu/header_menu.tsx +++ b/x-pack/plugins/observability/public/pages/overview/components/header_menu/header_menu.tsx @@ -8,7 +8,10 @@ import { EuiHeaderLink, EuiHeaderLinks } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { ObservabilityAIAssistantActionMenuItem } from '@kbn/observability-ai-assistant-plugin/public'; +import { + ObservabilityAIAssistantActionMenuItem, + useObservabilityAIAssistantOptional, +} from '@kbn/observability-ai-assistant-plugin/public'; import { useKibana } from '../../../../utils/kibana_react'; import { usePluginContext } from '../../../../hooks/use_plugin_context'; import HeaderMenuPortal from './header_menu_portal'; @@ -19,6 +22,8 @@ export function HeaderMenu(): React.ReactElement | null { appMountParameters: { setHeaderActionMenu }, } = usePluginContext(); + const aiAssistant = useObservabilityAIAssistantOptional(); + return ( @@ -29,7 +34,7 @@ export function HeaderMenu(): React.ReactElement | null { > {addDataLinkText} - + {aiAssistant?.isEnabled() ? : null} ); diff --git a/x-pack/plugins/observability/public/pages/rules/rules.test.tsx b/x-pack/plugins/observability/public/pages/rules/rules.test.tsx index d0d8678488aa6..4b5ee362e0aa5 100644 --- a/x-pack/plugins/observability/public/pages/rules/rules.test.tsx +++ b/x-pack/plugins/observability/public/pages/rules/rules.test.tsx @@ -31,7 +31,9 @@ jest.mock('@kbn/triggers-actions-ui-plugin/public', () => ({ })); jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({ - appMountParameters: {} as AppMountParameters, + appMountParameters: { + setHeaderActionMenu: () => {}, + } as unknown as AppMountParameters, config: { unsafe: { slo: { enabled: false }, @@ -47,12 +49,6 @@ jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({ compositeSlo: { enabled: false, }, - aiAssistant: { - enabled: false, - feedback: { - enabled: false, - }, - }, }, observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), ObservabilityPageTemplate: KibanaPageTemplate, diff --git a/x-pack/plugins/observability/public/pages/slo_details/components/slo_detail_alerts.tsx b/x-pack/plugins/observability/public/pages/slo_details/components/slo_detail_alerts.tsx index e233e667d7615..fe4a8c2e16136 100644 --- a/x-pack/plugins/observability/public/pages/slo_details/components/slo_detail_alerts.tsx +++ b/x-pack/plugins/observability/public/pages/slo_details/components/slo_detail_alerts.tsx @@ -8,7 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React, { Fragment } from 'react'; import { AlertConsumers } from '@kbn/rule-data-utils'; -import { SLOWithSummaryResponse } from '@kbn/slo-schema'; +import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; import { useKibana } from '../../../utils/kibana_react'; const ALERTS_TABLE_ID = 'xpack.observability.slo.sloDetails.alertTable'; @@ -34,7 +34,14 @@ export function SloDetailsAlerts({ slo }: Props) { flyoutSize="s" data-test-subj="alertTable" featureIds={[AlertConsumers.SLO]} - query={{ bool: { filter: { term: { 'slo.id': slo.id } } } }} + query={{ + bool: { + filter: [ + { term: { 'slo.id': slo.id } }, + { term: { 'slo.instanceId': slo.instanceId ?? ALL_VALUE } }, + ], + }, + }} showExpandToDetails={false} showAlertStatusWithFlapping pageSize={100} diff --git a/x-pack/plugins/observability/public/pages/slo_details/components/slo_details.tsx b/x-pack/plugins/observability/public/pages/slo_details/components/slo_details.tsx index 73bcf5e7d9b4d..f837627ed614f 100644 --- a/x-pack/plugins/observability/public/pages/slo_details/components/slo_details.tsx +++ b/x-pack/plugins/observability/public/pages/slo_details/components/slo_details.tsx @@ -40,7 +40,9 @@ type TabId = typeof OVERVIEW_TAB_ID | typeof ALERTS_TAB_ID; export function SloDetails({ slo, isAutoRefreshing }: Props) { const { search } = useLocation(); - const { data: activeAlerts } = useFetchActiveAlerts({ sloIds: [slo.id] }); + const { data: activeAlerts } = useFetchActiveAlerts({ + sloIdsAndInstanceIds: [[slo.id, slo.instanceId ?? ALL_VALUE]], + }); const { isLoading: historicalSummaryLoading, data: historicalSummaries = [] } = useFetchHistoricalSummary({ list: [{ sloId: slo.id, instanceId: slo.instanceId ?? ALL_VALUE }], @@ -104,7 +106,7 @@ export function SloDetails({ slo, isAutoRefreshing }: Props) { 'data-test-subj': 'alertsTab', append: ( - {(activeAlerts && activeAlerts[slo.id]?.count) ?? 0} + {(activeAlerts && activeAlerts.get(slo)) ?? 0} ), content: , diff --git a/x-pack/plugins/observability/public/pages/slo_details/slo_details.test.tsx b/x-pack/plugins/observability/public/pages/slo_details/slo_details.test.tsx index d403b98d7261c..c40b6f5265fec 100644 --- a/x-pack/plugins/observability/public/pages/slo_details/slo_details.test.tsx +++ b/x-pack/plugins/observability/public/pages/slo_details/slo_details.test.tsx @@ -14,7 +14,7 @@ import { useLicense } from '../../hooks/use_license'; import { useCapabilities } from '../../hooks/slo/use_capabilities'; import { useFetchSloDetails } from '../../hooks/slo/use_fetch_slo_details'; import { useFetchHistoricalSummary } from '../../hooks/slo/use_fetch_historical_summary'; -import { useFetchActiveAlerts } from '../../hooks/slo/use_fetch_active_alerts'; +import { ActiveAlerts, useFetchActiveAlerts } from '../../hooks/slo/use_fetch_active_alerts'; import { useCloneSlo } from '../../hooks/slo/use_clone_slo'; import { useDeleteSlo } from '../../hooks/slo/use_delete_slo'; import { render } from '../../utils/test_helper'; @@ -27,6 +27,7 @@ import { } from '../../data/slo/historical_summary_data'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { buildApmAvailabilityIndicator } from '../../data/slo/indicator'; +import { ALL_VALUE } from '@kbn/slo-schema'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -63,6 +64,7 @@ const mockDelete = jest.fn(); const mockKibana = () => { useKibanaMock.mockReturnValue({ services: { + theme: {}, application: { navigateToUrl: mockNavigate }, charts: chartPluginMock.createStartContract(), http: { @@ -109,7 +111,7 @@ describe('SLO Details Page', () => { isLoading: false, data: historicalSummaryData, }); - useFetchActiveAlertsMock.mockReturnValue({ isLoading: false, data: {} }); + useFetchActiveAlertsMock.mockReturnValue({ isLoading: false, data: new ActiveAlerts() }); useCloneSloMock.mockReturnValue({ mutate: mockClone }); useDeleteSloMock.mockReturnValue({ mutate: mockDelete }); useLocationMock.mockReturnValue({ search: '' }); @@ -299,7 +301,7 @@ describe('SLO Details Page', () => { useLicenseMock.mockReturnValue({ hasAtLeast: () => true }); useFetchActiveAlertsMock.mockReturnValue({ isLoading: false, - data: { [slo.id]: { count: 2, ruleIds: ['rule-1', 'rule-2'] } }, + data: new ActiveAlerts({ [`${slo.id}|${ALL_VALUE}`]: 2 }), }); render(); diff --git a/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.test.tsx b/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.test.tsx index c35bcf0368357..06d3285f8945d 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.test.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.test.tsx @@ -66,6 +66,7 @@ const mockBasePathPrepend = jest.fn(); const mockKibana = () => { useKibanaMock.mockReturnValue({ services: { + theme: {}, application: { navigateToUrl: mockNavigate, }, diff --git a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_badges.tsx b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_badges.tsx index f9c7cc3faeeb8..deccd010205a0 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_badges.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_badges.tsx @@ -15,12 +15,11 @@ import { SloStatusBadge } from '../../../../components/slo/slo_status_badge'; import { SloActiveAlertsBadge } from '../../../../components/slo/slo_status_badge/slo_active_alerts_badge'; import { SloTimeWindowBadge } from './slo_time_window_badge'; import { SloRulesBadge } from './slo_rules_badge'; -import type { ActiveAlerts } from '../../../../hooks/slo/use_fetch_active_alerts'; import type { SloRule } from '../../../../hooks/slo/use_fetch_rules_for_slo'; import { SloGroupByBadge } from '../../../../components/slo/slo_status_badge/slo_group_by_badge'; export interface Props { - activeAlerts?: ActiveAlerts; + activeAlerts?: number; isLoading: boolean; rules: Array> | undefined; slo: SLOWithSummaryResponse; diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx index 8d2e3d6f2b8f5..72656db6dcdc4 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx @@ -27,7 +27,6 @@ import { sloKeys } from '../../../hooks/slo/query_key_factory'; import { useCapabilities } from '../../../hooks/slo/use_capabilities'; import { useCloneSlo } from '../../../hooks/slo/use_clone_slo'; import { useDeleteSlo } from '../../../hooks/slo/use_delete_slo'; -import type { ActiveAlerts } from '../../../hooks/slo/use_fetch_active_alerts'; import type { SloRule } from '../../../hooks/slo/use_fetch_rules_for_slo'; import { useGetFilteredRuleTypes } from '../../../hooks/use_get_filtered_rule_types'; import type { RulesParams } from '../../../locators/rules'; @@ -46,7 +45,7 @@ export interface SloListItemProps { rules: Array> | undefined; historicalSummary?: HistoricalSummaryResponse[]; historicalSummaryLoading: boolean; - activeAlerts?: ActiveAlerts; + activeAlerts?: number; } export function SloListItem({ diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.tsx index 2b568d0ca45a3..4f491f6b8fca2 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.tsx @@ -21,10 +21,14 @@ export interface Props { } export function SloListItems({ sloList, loading, error }: Props) { - const sloIds = sloList.map((slo) => slo.id); + const sloIdsAndInstanceIds = sloList.map( + (slo) => [slo.id, slo.instanceId ?? ALL_VALUE] as [string, string] + ); - const { data: activeAlertsBySlo } = useFetchActiveAlerts({ sloIds }); - const { data: rulesBySlo } = useFetchRulesForSlo({ sloIds }); + const { data: activeAlertsBySlo } = useFetchActiveAlerts({ sloIdsAndInstanceIds }); + const { data: rulesBySlo } = useFetchRulesForSlo({ + sloIds: sloIdsAndInstanceIds.map((item) => item[0]), + }); const { isLoading: historicalSummaryLoading, data: historicalSummaries = [] } = useFetchHistoricalSummary({ list: sloList.map((slo) => ({ sloId: slo.id, instanceId: slo.instanceId ?? ALL_VALUE })), @@ -42,7 +46,7 @@ export function SloListItems({ sloList, loading, error }: Props) { {sloList.map((slo) => (
Add rule flyou const mockKibana = () => { useKibanaMock.mockReturnValue({ services: { + theme: {}, application: { navigateToUrl: mockNavigate }, charts: chartPluginMock.createSetupContract(), data: { diff --git a/x-pack/plugins/observability/public/pages/slos_welcome/slos_welcome.test.tsx b/x-pack/plugins/observability/public/pages/slos_welcome/slos_welcome.test.tsx index f4b7e1ec6225c..155e49c3976fc 100644 --- a/x-pack/plugins/observability/public/pages/slos_welcome/slos_welcome.test.tsx +++ b/x-pack/plugins/observability/public/pages/slos_welcome/slos_welcome.test.tsx @@ -36,6 +36,7 @@ const mockNavigate = jest.fn(); const mockKibana = () => { useKibanaMock.mockReturnValue({ services: { + theme: {}, application: { navigateToUrl: mockNavigate }, http: { basePath: { diff --git a/x-pack/plugins/observability/server/assets/component_templates/slo_settings_template.ts b/x-pack/plugins/observability/server/assets/component_templates/slo_settings_template.ts index e2adb74dd1104..9b0a6931e7c15 100644 --- a/x-pack/plugins/observability/server/assets/component_templates/slo_settings_template.ts +++ b/x-pack/plugins/observability/server/assets/component_templates/slo_settings_template.ts @@ -11,7 +11,7 @@ export const getSLOSettingsTemplate = (name: string) => ({ name, template: { settings: { - auto_expand_replicas: '0-all', + auto_expand_replicas: '0-1', hidden: true, }, }, diff --git a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.test.ts b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.test.ts index 745734ca42a6e..47277a003f393 100644 --- a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.test.ts +++ b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.test.ts @@ -25,11 +25,6 @@ import { MockedLogger } from '@kbn/logging-mocks'; import { SanitizedRuleConfig } from '@kbn/alerting-plugin/common'; import { Alert, RuleExecutorServices } from '@kbn/alerting-plugin/server'; import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_settings'; -import { - ALERT_EVALUATION_THRESHOLD, - ALERT_EVALUATION_VALUE, - ALERT_REASON, -} from '@kbn/rule-data-utils'; import { LocatorPublic } from '@kbn/share-plugin/common'; import type { AlertsLocatorParams } from '../../../../common'; import { getRuleExecutor } from './executor'; @@ -44,7 +39,6 @@ import { BurnRateRuleParams, AlertStates, } from './types'; -import { SLO_ID_FIELD, SLO_REVISION_FIELD } from '../../../../common/field_names/slo'; import { SLONotFound } from '../../../errors'; import { SO_SLO_TYPE } from '../../../saved_objects'; import { sloSchema } from '@kbn/slo-schema'; @@ -53,6 +47,26 @@ import { ALERT_ACTION_ID, HIGH_PRIORITY_ACTION_ID, } from '../../../../common/constants'; +import { EvaluationBucket } from './lib/evaluate'; +import { + SLO_ID_FIELD, + SLO_INSTANCE_ID_FIELD, + SLO_REVISION_FIELD, +} from '../../../../common/field_names/slo'; +import { + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, + ALERT_REASON, +} from '@kbn/rule-registry-plugin/common/technical_rule_data_field_names'; +import { + generateAboveThresholdKey, + generateBurnRateKey, + generateStatsKey, + generateWindowId, + LONG_WINDOW, + SHORT_WINDOW, +} from './lib/build_query'; +import { get } from 'lodash'; const commonEsResponse = { took: 100, @@ -184,14 +198,35 @@ describe('BurnRateRuleExecutor', () => { it('does not schedule an alert when long windows burn rates are below the threshold', async () => { const slo = createSLO({ objective: { target: 0.9 } }); + const ruleParams = someRuleParamsWithWindows({ sloId: slo.id }); soClientMock.find.mockResolvedValueOnce(createFindResponse([slo])); - esClientMock.search.mockResolvedValueOnce(generateEsResponse(slo, 2.1, 1.9)); - esClientMock.search.mockResolvedValueOnce(generateEsResponse(slo, 1.1, 0.9)); + const buckets = [ + { + instanceId: 'foo', + windows: [ + { shortWindowBurnRate: 2.1, longWindowBurnRate: 0.9 }, + { shortWindowBurnRate: 1.2, longWindowBurnRate: 0.9 }, + ], + }, + { + instanceId: 'bar', + windows: [ + { shortWindowBurnRate: 2.1, longWindowBurnRate: 0.9 }, + { shortWindowBurnRate: 1.2, longWindowBurnRate: 0.9 }, + ], + }, + ]; + esClientMock.search.mockResolvedValueOnce( + generateEsResponse(ruleParams, buckets, { instanceId: 'bar' }) + ); + esClientMock.search.mockResolvedValueOnce( + generateEsResponse(ruleParams, [], { instanceId: 'bar' }) + ); alertFactoryMock.done.mockReturnValueOnce({ getRecoveredAlerts: () => [] }); const executor = getRuleExecutor({ basePath: basePathMock }); await executor({ - params: someRuleParamsWithWindows({ sloId: slo.id }), + params: ruleParams, startedAt: new Date(), services: servicesMock, executionId: 'irrelevant', @@ -208,14 +243,35 @@ describe('BurnRateRuleExecutor', () => { it('does not schedule an alert when the short window burn rate is below the threshold', async () => { const slo = createSLO({ objective: { target: 0.9 } }); + const ruleParams = someRuleParamsWithWindows({ sloId: slo.id }); soClientMock.find.mockResolvedValueOnce(createFindResponse([slo])); - esClientMock.search.mockResolvedValue(generateEsResponse(slo, 1.9, 2.1)); - esClientMock.search.mockResolvedValue(generateEsResponse(slo, 0.9, 1.1)); + const buckets = [ + { + instanceId: 'foo', + windows: [ + { shortWindowBurnRate: 0.9, longWindowBurnRate: 2.1 }, + { shortWindowBurnRate: 0.9, longWindowBurnRate: 1.2 }, + ], + }, + { + instanceId: 'bar', + windows: [ + { shortWindowBurnRate: 0.9, longWindowBurnRate: 2.1 }, + { shortWindowBurnRate: 0.9, longWindowBurnRate: 1.2 }, + ], + }, + ]; + esClientMock.search.mockResolvedValueOnce( + generateEsResponse(ruleParams, buckets, { instanceId: 'bar' }) + ); + esClientMock.search.mockResolvedValueOnce( + generateEsResponse(ruleParams, [], { instanceId: 'bar' }) + ); alertFactoryMock.done.mockReturnValueOnce({ getRecoveredAlerts: () => [] }); const executor = getRuleExecutor({ basePath: basePathMock }); await executor({ - params: someRuleParamsWithWindows({ sloId: slo.id }), + params: ruleParams, startedAt: new Date(), services: servicesMock, executionId: 'irrelevant', @@ -232,9 +288,30 @@ describe('BurnRateRuleExecutor', () => { it('schedules an alert when both windows of first window definition burn rate have reached the threshold', async () => { const slo = createSLO({ objective: { target: 0.9 } }); + const ruleParams = someRuleParamsWithWindows({ sloId: slo.id }); soClientMock.find.mockResolvedValueOnce(createFindResponse([slo])); - esClientMock.search.mockResolvedValue(generateEsResponse(slo, 2, 2)); - esClientMock.search.mockResolvedValue(generateEsResponse(slo, 2, 2)); + const buckets = [ + { + instanceId: 'foo', + windows: [ + { shortWindowBurnRate: 2.1, longWindowBurnRate: 2.3 }, + { shortWindowBurnRate: 0.9, longWindowBurnRate: 1.2 }, + ], + }, + { + instanceId: 'bar', + windows: [ + { shortWindowBurnRate: 2.2, longWindowBurnRate: 2.5 }, + { shortWindowBurnRate: 0.9, longWindowBurnRate: 1.2 }, + ], + }, + ]; + esClientMock.search.mockResolvedValueOnce( + generateEsResponse(ruleParams, buckets, { instanceId: 'bar' }) + ); + esClientMock.search.mockResolvedValueOnce( + generateEsResponse(ruleParams, [], { instanceId: 'bar' }) + ); const alertMock: Partial = { scheduleActions: jest.fn(), replaceState: jest.fn(), @@ -247,7 +324,7 @@ describe('BurnRateRuleExecutor', () => { alertsLocator: alertsLocatorMock, }); await executor({ - params: someRuleParamsWithWindows({ sloId: slo.id }), + params: ruleParams, startedAt: new Date(), services: servicesMock, executionId: 'irrelevant', @@ -260,24 +337,48 @@ describe('BurnRateRuleExecutor', () => { }); expect(alertWithLifecycleMock).toBeCalledWith({ - id: `alert-${slo.id}-${slo.revision}`, + id: 'foo', fields: { [ALERT_REASON]: - 'CRITICAL: The burn rate for the past 1h is 2 and for the past 5m is 2. Alert when above 2 for both windows', + 'CRITICAL: The burn rate for the past 1h is 2.3 and for the past 5m is 2.1 for foo. Alert when above 2 for both windows', [ALERT_EVALUATION_THRESHOLD]: 2, - [ALERT_EVALUATION_VALUE]: 2, + [ALERT_EVALUATION_VALUE]: 2.1, [SLO_ID_FIELD]: slo.id, [SLO_REVISION_FIELD]: slo.revision, + [SLO_INSTANCE_ID_FIELD]: 'foo', + }, + }); + expect(alertWithLifecycleMock).toBeCalledWith({ + id: 'bar', + fields: { + [ALERT_REASON]: + 'CRITICAL: The burn rate for the past 1h is 2.5 and for the past 5m is 2.2 for bar. Alert when above 2 for both windows', + [ALERT_EVALUATION_THRESHOLD]: 2, + [ALERT_EVALUATION_VALUE]: 2.2, + [SLO_ID_FIELD]: slo.id, + [SLO_REVISION_FIELD]: slo.revision, + [SLO_INSTANCE_ID_FIELD]: 'bar', }, }); expect(alertMock.scheduleActions).toBeCalledWith( ALERT_ACTION.id, expect.objectContaining({ - longWindow: { burnRate: 2, duration: '1h' }, - shortWindow: { burnRate: 2, duration: '5m' }, + longWindow: { burnRate: 2.3, duration: '1h' }, + shortWindow: { burnRate: 2.1, duration: '5m' }, burnRateThreshold: 2, reason: - 'CRITICAL: The burn rate for the past 1h is 2 and for the past 5m is 2. Alert when above 2 for both windows', + 'CRITICAL: The burn rate for the past 1h is 2.3 and for the past 5m is 2.1 for foo. Alert when above 2 for both windows', + alertDetailsUrl: 'mockedAlertsLocator > getLocation', + }) + ); + expect(alertMock.scheduleActions).toBeCalledWith( + ALERT_ACTION.id, + expect.objectContaining({ + longWindow: { burnRate: 2.5, duration: '1h' }, + shortWindow: { burnRate: 2.2, duration: '5m' }, + burnRateThreshold: 2, + reason: + 'CRITICAL: The burn rate for the past 1h is 2.5 and for the past 5m is 2.2 for bar. Alert when above 2 for both windows', alertDetailsUrl: 'mockedAlertsLocator > getLocation', }) ); @@ -292,9 +393,30 @@ describe('BurnRateRuleExecutor', () => { it('schedules an alert when both windows of second window definition burn rate have reached the threshold', async () => { const slo = createSLO({ objective: { target: 0.9 } }); + const ruleParams = someRuleParamsWithWindows({ sloId: slo.id }); soClientMock.find.mockResolvedValueOnce(createFindResponse([slo])); - esClientMock.search.mockResolvedValue(generateEsResponse(slo, 1.5, 1.5)); - esClientMock.search.mockResolvedValue(generateEsResponse(slo, 1.5, 1.5)); + const buckets = [ + { + instanceId: 'foo', + windows: [ + { shortWindowBurnRate: 1.0, longWindowBurnRate: 2.0 }, + { shortWindowBurnRate: 1.9, longWindowBurnRate: 1.2 }, + ], + }, + { + instanceId: 'bar', + windows: [ + { shortWindowBurnRate: 1.0, longWindowBurnRate: 2.0 }, + { shortWindowBurnRate: 1.5, longWindowBurnRate: 1.1 }, + ], + }, + ]; + esClientMock.search.mockResolvedValueOnce( + generateEsResponse(ruleParams, buckets, { instanceId: 'bar' }) + ); + esClientMock.search.mockResolvedValueOnce( + generateEsResponse(ruleParams, [], { instanceId: 'bar' }) + ); const alertMock: Partial = { scheduleActions: jest.fn(), replaceState: jest.fn(), @@ -304,7 +426,7 @@ describe('BurnRateRuleExecutor', () => { const executor = getRuleExecutor({ basePath: basePathMock }); await executor({ - params: someRuleParamsWithWindows({ sloId: slo.id }), + params: ruleParams, startedAt: new Date(), services: servicesMock, executionId: 'irrelevant', @@ -317,72 +439,50 @@ describe('BurnRateRuleExecutor', () => { }); expect(alertWithLifecycleMock).toBeCalledWith({ - id: `alert-${slo.id}-${slo.revision}`, + id: 'foo', fields: { [ALERT_REASON]: - 'HIGH: The burn rate for the past 6h is 1.5 and for the past 30m is 1.5. Alert when above 1 for both windows', + 'HIGH: The burn rate for the past 6h is 1.2 and for the past 30m is 1.9 for foo. Alert when above 1 for both windows', [ALERT_EVALUATION_THRESHOLD]: 1, - [ALERT_EVALUATION_VALUE]: 1.5, + [ALERT_EVALUATION_VALUE]: 1.2, [SLO_ID_FIELD]: slo.id, [SLO_REVISION_FIELD]: slo.revision, + [SLO_INSTANCE_ID_FIELD]: 'foo', + }, + }); + expect(alertWithLifecycleMock).toBeCalledWith({ + id: 'bar', + fields: { + [ALERT_REASON]: + 'HIGH: The burn rate for the past 6h is 1.1 and for the past 30m is 1.5 for bar. Alert when above 1 for both windows', + [ALERT_EVALUATION_THRESHOLD]: 1, + [ALERT_EVALUATION_VALUE]: 1.1, + [SLO_ID_FIELD]: slo.id, + [SLO_REVISION_FIELD]: slo.revision, + [SLO_INSTANCE_ID_FIELD]: 'bar', }, }); expect(alertMock.scheduleActions).toBeCalledWith( HIGH_PRIORITY_ACTION_ID, expect.objectContaining({ - longWindow: { burnRate: 1.5, duration: '6h' }, - shortWindow: { burnRate: 1.5, duration: '30m' }, + longWindow: { burnRate: 1.2, duration: '6h' }, + shortWindow: { burnRate: 1.9, duration: '30m' }, burnRateThreshold: 1, reason: - 'HIGH: The burn rate for the past 6h is 1.5 and for the past 30m is 1.5. Alert when above 1 for both windows', + 'HIGH: The burn rate for the past 6h is 1.2 and for the past 30m is 1.9 for foo. Alert when above 1 for both windows', }) ); - expect(alertMock.replaceState).toBeCalledWith({ alertState: AlertStates.ALERT }); - }); - - it('sets the context on the recovered alerts using the last window', async () => { - const slo = createSLO({ objective: { target: 0.9 } }); - soClientMock.find.mockResolvedValueOnce(createFindResponse([slo])); - esClientMock.search.mockResolvedValue(generateEsResponse(slo, 0.9, 0.9)); - esClientMock.search.mockResolvedValue(generateEsResponse(slo, 0.9, 0.9)); - const alertMock: Partial = { - setContext: jest.fn(), - }; - alertFactoryMock.done.mockReturnValueOnce({ getRecoveredAlerts: () => [alertMock] as any }); - - const executor = getRuleExecutor({ - basePath: basePathMock, - alertsLocator: alertsLocatorMock, - }); - - await executor({ - params: someRuleParamsWithWindows({ sloId: slo.id }), - startedAt: new Date(), - services: servicesMock, - executionId: 'irrelevant', - logger: loggerMock, - previousStartedAt: null, - rule: {} as SanitizedRuleConfig, - spaceId: 'irrelevant', - state: {}, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - }); - - expect(alertWithLifecycleMock).not.toBeCalled(); - expect(alertMock.setContext).toBeCalledWith( + expect(alertMock.scheduleActions).toBeCalledWith( + HIGH_PRIORITY_ACTION_ID, expect.objectContaining({ - longWindow: { burnRate: 0.9, duration: '6h' }, - shortWindow: { burnRate: 0.9, duration: '30m' }, + longWindow: { burnRate: 1.1, duration: '6h' }, + shortWindow: { burnRate: 1.5, duration: '30m' }, burnRateThreshold: 1, - alertDetailsUrl: 'mockedAlertsLocator > getLocation', + reason: + 'HIGH: The burn rate for the past 6h is 1.1 and for the past 30m is 1.5 for bar. Alert when above 1 for both windows', }) ); - expect(alertsLocatorMock.getLocation).toBeCalledWith({ - baseUrl: 'https://kibana.dev', - kuery: 'kibana.alert.uuid: "mockedAlertUuid"', - rangeFrom: expect.stringMatching(ISO_DATE_REGEX), - spaceId: 'irrelevant', - }); + expect(alertMock.replaceState).toBeCalledWith({ alertState: AlertStates.ALERT }); }); }); }); @@ -412,18 +512,79 @@ function someRuleParamsWithWindows(params: Partial = {}): Bu }; } -function generateEsResponse(slo: SLO, shortWindowBurnRate: number, longWindowBurnRate: number) { +interface ResponseBucket { + instanceId: string; + windows: Array<{ + shortWindowBurnRate: number; + longWindowBurnRate: number; + }>; +} + +interface AfterKey { + instanceId: string; +} + +function generateEsResponse( + params: BurnRateRuleParams, + buckets: ResponseBucket[], + afterKey: AfterKey +) { return { ...commonEsResponse, aggregations: { - SHORT_WINDOW: { buckets: [generateBucketForBurnRate(slo, shortWindowBurnRate)] }, - LONG_WINDOW: { buckets: [generateBucketForBurnRate(slo, longWindowBurnRate)] }, + instances: { + after: afterKey, + doc_count: buckets.length ? 100 : 0, + buckets: buckets + .map((bucket) => { + return bucket.windows.reduce( + (acc, win, index) => ({ + ...acc, + [generateStatsKey(generateWindowId(index), SHORT_WINDOW)]: { + doc_count: 100, + good: { value: win.shortWindowBurnRate * 100 }, + total: { value: 100 }, + }, + [generateStatsKey(generateWindowId(index), LONG_WINDOW)]: { + doc_count: 100, + good: { value: win.longWindowBurnRate * 100 }, + total: { value: 100 }, + }, + [generateBurnRateKey(generateWindowId(index), SHORT_WINDOW)]: { + value: win.shortWindowBurnRate, + }, + [generateBurnRateKey(generateWindowId(index), LONG_WINDOW)]: { + value: win.longWindowBurnRate, + }, + [generateAboveThresholdKey(generateWindowId(index), SHORT_WINDOW)]: { + value: win.shortWindowBurnRate >= params.windows[index].burnRateThreshold ? 1 : 0, + }, + [generateAboveThresholdKey(generateWindowId(index), LONG_WINDOW)]: { + value: win.longWindowBurnRate >= params.windows[index].burnRateThreshold ? 1 : 0, + }, + }), + { + key: { instanceId: bucket.instanceId }, + doc_count: 100, + } as EvaluationBucket + ); + }) + .filter((bucket: any) => + params.windows.some( + (_win, index) => + get( + bucket, + [generateAboveThresholdKey(generateWindowId(index), SHORT_WINDOW), 'value'], + 0 + ) === 1 && + get( + bucket, + [generateAboveThresholdKey(generateWindowId(index), LONG_WINDOW), 'value'], + 0 + ) === 1 + ) + ), + }, }, }; } - -function generateBucketForBurnRate(slo: SLO, burnRate: number) { - const total = 100; - const good = total * (1 - burnRate + slo.objective.target * burnRate); - return { good: { value: good }, total: { value: total } }; -} diff --git a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.ts b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.ts index 42841d276c098..22e5594f0fc19 100644 --- a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.ts +++ b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.ts @@ -17,14 +17,17 @@ import { ExecutorType } from '@kbn/alerting-plugin/server'; import { IBasePath } from '@kbn/core/server'; import { LocatorPublic } from '@kbn/share-plugin/common'; -import { memoize, last, upperCase } from 'lodash'; +import { upperCase } from 'lodash'; import { addSpaceIdToPath } from '@kbn/spaces-plugin/server'; import { ALL_VALUE } from '@kbn/slo-schema'; import { AlertsLocatorParams, getAlertUrl } from '../../../../common'; -import { SLO_ID_FIELD, SLO_REVISION_FIELD } from '../../../../common/field_names/slo'; -import { Duration, SLO, toDurationUnit } from '../../../domain/models'; -import { DefaultSLIClient, KibanaSavedObjectsSLORepository } from '../../../services/slo'; -import { computeBurnRate } from '../../../domain/services'; +import { + SLO_ID_FIELD, + SLO_INSTANCE_ID_FIELD, + SLO_REVISION_FIELD, +} from '../../../../common/field_names/slo'; +import { Duration } from '../../../domain/models'; +import { KibanaSavedObjectsSLORepository } from '../../../services/slo'; import { AlertStates, BurnRateAlertContext, @@ -40,57 +43,7 @@ import { MEDIUM_PRIORITY_ACTION, LOW_PRIORITY_ACTION, } from '../../../../common/constants'; - -const SHORT_WINDOW = 'SHORT_WINDOW'; -const LONG_WINDOW = 'LONG_WINDOW'; - -async function evaluateWindow(slo: SLO, summaryClient: DefaultSLIClient, windowDef: WindowSchema) { - const longWindowDuration = new Duration( - windowDef.longWindow.value, - toDurationUnit(windowDef.longWindow.unit) - ); - const shortWindowDuration = new Duration( - windowDef.shortWindow.value, - toDurationUnit(windowDef.shortWindow.unit) - ); - - const sliData = await summaryClient.fetchSLIDataFrom(slo, ALL_VALUE, [ - { name: LONG_WINDOW, duration: longWindowDuration.add(slo.settings.syncDelay) }, - { name: SHORT_WINDOW, duration: shortWindowDuration.add(slo.settings.syncDelay) }, - ]); - - const longWindowBurnRate = computeBurnRate(slo, sliData[LONG_WINDOW]); - const shortWindowBurnRate = computeBurnRate(slo, sliData[SHORT_WINDOW]); - - const shouldAlert = - longWindowBurnRate >= windowDef.burnRateThreshold && - shortWindowBurnRate >= windowDef.burnRateThreshold; - - return { - shouldAlert, - longWindowBurnRate, - shortWindowBurnRate, - longWindowDuration, - shortWindowDuration, - window: windowDef, - }; -} - -async function evaluate(slo: SLO, summaryClient: DefaultSLIClient, params: BurnRateRuleParams) { - const evalWindow = memoize(async (windowDef: WindowSchema) => - evaluateWindow(slo, summaryClient, windowDef) - ); - for (const windowDef of params.windows) { - const result = await evalWindow(windowDef); - if (result.shouldAlert) { - return result; - } - } - // If none of the previous windows match, we need to return the last window - // for the recovery context. Since evalWindow is memoized, it shouldn't make - // and additional call to evaulateWindow. - return await evalWindow(last(params.windows) as WindowSchema); -} +import { evaluate } from './lib/evaluate'; export const getRuleExecutor = ({ basePath, @@ -129,85 +82,91 @@ export const getRuleExecutor = ({ } = services; const sloRepository = new KibanaSavedObjectsSLORepository(soClient); - const summaryClient = new DefaultSLIClient(esClient.asCurrentUser); const slo = await sloRepository.findById(params.sloId); if (!slo.enabled) { return { state: {} }; } - const result = await evaluate(slo, summaryClient, params); - - if (result) { - const { - shouldAlert, - longWindowDuration, - longWindowBurnRate, - shortWindowDuration, - shortWindowBurnRate, - window: windowDef, - } = result; - - const viewInAppUrl = addSpaceIdToPath( - basePath.publicBaseUrl, - spaceId, - `/app/observability/slos/${slo.id}` - ); + const results = await evaluate(esClient.asCurrentUser, slo, params, startedAt); - if (shouldAlert) { - const reason = buildReason( - windowDef.actionGroup, + if (results.length > 0) { + for (const result of results) { + const { + instanceId, + shouldAlert, longWindowDuration, longWindowBurnRate, shortWindowDuration, shortWindowBurnRate, - windowDef - ); + window: windowDef, + } = result; - const alertId = `alert-${slo.id}-${slo.revision}`; - const indexedStartedAt = getAlertStartedDate(alertId) ?? startedAt.toISOString(); - const alertUuid = getAlertUuid(alertId); - const alertDetailsUrl = await getAlertUrl( - alertUuid, + const urlQuery = instanceId === ALL_VALUE ? '' : `?instanceId=${instanceId}`; + const viewInAppUrl = addSpaceIdToPath( + basePath.publicBaseUrl, spaceId, - indexedStartedAt, - alertsLocator, - basePath.publicBaseUrl + `/app/observability/slos/${slo.id}${urlQuery}` ); - - const context = { - alertDetailsUrl, - reason, - longWindow: { burnRate: longWindowBurnRate, duration: longWindowDuration.format() }, - shortWindow: { burnRate: shortWindowBurnRate, duration: shortWindowDuration.format() }, - burnRateThreshold: windowDef.burnRateThreshold, - timestamp: startedAt.toISOString(), - viewInAppUrl, - sloId: slo.id, - sloName: slo.name, - }; - - const alert = alertWithLifecycle({ - id: alertId, - fields: { - [ALERT_REASON]: reason, - [ALERT_EVALUATION_THRESHOLD]: windowDef.burnRateThreshold, - [ALERT_EVALUATION_VALUE]: Math.min(longWindowBurnRate, shortWindowBurnRate), - [SLO_ID_FIELD]: slo.id, - [SLO_REVISION_FIELD]: slo.revision, - }, - }); - - alert.scheduleActions(windowDef.actionGroup, context); - alert.replaceState({ alertState: AlertStates.ALERT }); + if (shouldAlert) { + const reason = buildReason( + instanceId, + windowDef.actionGroup, + longWindowDuration, + longWindowBurnRate, + shortWindowDuration, + shortWindowBurnRate, + windowDef + ); + + const alertId = instanceId; + const indexedStartedAt = getAlertStartedDate(alertId) ?? startedAt.toISOString(); + const alertUuid = getAlertUuid(alertId); + const alertDetailsUrl = await getAlertUrl( + alertUuid, + spaceId, + indexedStartedAt, + alertsLocator, + basePath.publicBaseUrl + ); + + const context = { + alertDetailsUrl, + reason, + longWindow: { burnRate: longWindowBurnRate, duration: longWindowDuration.format() }, + shortWindow: { burnRate: shortWindowBurnRate, duration: shortWindowDuration.format() }, + burnRateThreshold: windowDef.burnRateThreshold, + timestamp: startedAt.toISOString(), + viewInAppUrl, + sloId: slo.id, + sloName: slo.name, + sloInstanceId: instanceId, + }; + + const alert = alertWithLifecycle({ + id: alertId, + + fields: { + [ALERT_REASON]: reason, + [ALERT_EVALUATION_THRESHOLD]: windowDef.burnRateThreshold, + [ALERT_EVALUATION_VALUE]: Math.min(longWindowBurnRate, shortWindowBurnRate), + [SLO_ID_FIELD]: slo.id, + [SLO_REVISION_FIELD]: slo.revision, + [SLO_INSTANCE_ID_FIELD]: instanceId, + }, + }); + + alert.scheduleActions(windowDef.actionGroup, context); + alert.replaceState({ alertState: AlertStates.ALERT }); + } } const { getRecoveredAlerts } = alertFactory.done(); const recoveredAlerts = getRecoveredAlerts(); for (const recoveredAlert of recoveredAlerts) { - const alertId = `alert-${slo.id}-${slo.revision}`; + const alertId = recoveredAlert.getId(); const indexedStartedAt = getAlertStartedDate(alertId) ?? startedAt.toISOString(); - const alertUuid = getAlertUuid(alertId); + const alertUuid = recoveredAlert.getUuid(); const alertDetailsUrl = await getAlertUrl( alertUuid, spaceId, @@ -215,15 +174,21 @@ export const getRuleExecutor = ({ alertsLocator, basePath.publicBaseUrl ); + + const urlQuery = alertId === ALL_VALUE ? '' : `?instanceId=${alertId}`; + const viewInAppUrl = addSpaceIdToPath( + basePath.publicBaseUrl, + spaceId, + `/app/observability/slos/${slo.id}${urlQuery}` + ); + const context = { - longWindow: { burnRate: longWindowBurnRate, duration: longWindowDuration.format() }, - shortWindow: { burnRate: shortWindowBurnRate, duration: shortWindowDuration.format() }, - burnRateThreshold: windowDef.burnRateThreshold, timestamp: startedAt.toISOString(), viewInAppUrl, alertDetailsUrl, sloId: slo.id, sloName: slo.name, + sloInstanceId: alertId, }; recoveredAlert.setContext(context); @@ -247,6 +212,7 @@ function getActionGroupName(id: string) { } function buildReason( + instanceId: string, actionGroup: string, longWindowDuration: Duration, longWindowBurnRate: number, @@ -254,9 +220,23 @@ function buildReason( shortWindowBurnRate: number, windowDef: WindowSchema ) { - return i18n.translate('xpack.observability.slo.alerting.burnRate.reason', { + if (instanceId === ALL_VALUE) { + return i18n.translate('xpack.observability.slo.alerting.burnRate.reason', { + defaultMessage: + '{actionGroupName}: The burn rate for the past {longWindowDuration} is {longWindowBurnRate} and for the past {shortWindowDuration} is {shortWindowBurnRate}. Alert when above {burnRateThreshold} for both windows', + values: { + actionGroupName: upperCase(getActionGroupName(actionGroup)), + longWindowDuration: longWindowDuration.format(), + longWindowBurnRate: numeral(longWindowBurnRate).format('0.[00]'), + shortWindowDuration: shortWindowDuration.format(), + shortWindowBurnRate: numeral(shortWindowBurnRate).format('0.[00]'), + burnRateThreshold: windowDef.burnRateThreshold, + }, + }); + } + return i18n.translate('xpack.observability.slo.alerting.burnRate.reasonForInstanceId', { defaultMessage: - '{actionGroupName}: The burn rate for the past {longWindowDuration} is {longWindowBurnRate} and for the past {shortWindowDuration} is {shortWindowBurnRate}. Alert when above {burnRateThreshold} for both windows', + '{actionGroupName}: The burn rate for the past {longWindowDuration} is {longWindowBurnRate} and for the past {shortWindowDuration} is {shortWindowBurnRate} for {instanceId}. Alert when above {burnRateThreshold} for both windows', values: { actionGroupName: upperCase(getActionGroupName(actionGroup)), longWindowDuration: longWindowDuration.format(), @@ -264,6 +244,7 @@ function buildReason( shortWindowDuration: shortWindowDuration.format(), shortWindowBurnRate: numeral(shortWindowBurnRate).format('0.[00]'), burnRateThreshold: windowDef.burnRateThreshold, + instanceId, }, }); } diff --git a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/field_map.ts b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/field_map.ts index d62cf74c0a356..d9aa34a0e9e8c 100644 --- a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/field_map.ts +++ b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/field_map.ts @@ -5,7 +5,11 @@ * 2.0. */ -import { SLO_ID_FIELD, SLO_REVISION_FIELD } from '../../../../common/field_names/slo'; +import { + SLO_ID_FIELD, + SLO_INSTANCE_ID_FIELD, + SLO_REVISION_FIELD, +} from '../../../../common/field_names/slo'; export const sloRuleFieldMap = { [SLO_ID_FIELD]: { @@ -18,6 +22,11 @@ export const sloRuleFieldMap = { array: false, required: false, }, + [SLO_INSTANCE_ID_FIELD]: { + type: 'keyword', + array: false, + required: false, + }, }; export type SLORuleFieldMap = typeof sloRuleFieldMap; diff --git a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/fixtures/rule.ts b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/fixtures/rule.ts new file mode 100644 index 0000000000000..f31a8ea948b80 --- /dev/null +++ b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/fixtures/rule.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { v4 } from 'uuid'; +import { + ALERT_ACTION, + HIGH_PRIORITY_ACTION, + LOW_PRIORITY_ACTION, + MEDIUM_PRIORITY_ACTION, +} from '../../../../../common/constants'; +import { SLO } from '../../../../domain/models'; +import { BurnRateRuleParams } from '../types'; + +export function createBurnRateRule(slo: SLO, params: Partial = {}) { + return { + sloId: slo.id, + windows: [ + { + id: v4(), + burnRateThreshold: 14.4, + maxBurnRateThreshold: null, + longWindow: { value: 1, unit: 'h' }, + shortWindow: { value: 5, unit: 'm' }, + actionGroup: ALERT_ACTION.id, + }, + { + id: v4(), + burnRateThreshold: 6, + maxBurnRateThreshold: null, + longWindow: { value: 6, unit: 'h' }, + shortWindow: { value: 30, unit: 'm' }, + actionGroup: HIGH_PRIORITY_ACTION.id, + }, + { + id: v4(), + burnRateThreshold: 3, + maxBurnRateThreshold: null, + longWindow: { value: 24, unit: 'h' }, + shortWindow: { value: 120, unit: 'm' }, + actionGroup: MEDIUM_PRIORITY_ACTION.id, + }, + { + id: v4(), + burnRateThreshold: 1, + maxBurnRateThreshold: null, + longWindow: { value: 72, unit: 'h' }, + shortWindow: { value: 360, unit: 'm' }, + actionGroup: LOW_PRIORITY_ACTION.id, + }, + ], + ...params, + }; +} diff --git a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/lib/__snapshots__/build_query.test.ts.snap b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/lib/__snapshots__/build_query.test.ts.snap new file mode 100644 index 0000000000000..8699b28ab5a5e --- /dev/null +++ b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/lib/__snapshots__/build_query.test.ts.snap @@ -0,0 +1,1375 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`buildQuery() should return a valid query for occurrences 1`] = ` +Object { + "aggs": Object { + "instances": Object { + "aggs": Object { + "WINDOW_0_LONG": Object { + "aggs": Object { + "good": Object { + "sum": Object { + "field": "slo.numerator", + }, + }, + "total": Object { + "sum": Object { + "field": "slo.denominator", + }, + }, + }, + "filter": Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-31T23:00:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + }, + "WINDOW_0_LONG_ABOVE_THRESHOLD": Object { + "bucket_script": Object { + "buckets_path": Object { + "burnRate": "WINDOW_0_LONG_BURN_RATE", + }, + "script": Object { + "params": Object { + "threshold": 14.4, + }, + "source": "params.burnRate >= params.threshold ? 1 : 0", + }, + }, + }, + "WINDOW_0_LONG_BURN_RATE": Object { + "bucket_script": Object { + "buckets_path": Object { + "good": "WINDOW_0_LONG>good", + "total": "WINDOW_0_LONG>total", + }, + "script": Object { + "params": Object { + "target": 0.999, + }, + "source": "params.total != null && params.total > 0 ? (1 - (params.good / params.total)) / (1 - params.target) : 0", + }, + }, + }, + "WINDOW_0_SHORT": Object { + "aggs": Object { + "good": Object { + "sum": Object { + "field": "slo.numerator", + }, + }, + "total": Object { + "sum": Object { + "field": "slo.denominator", + }, + }, + }, + "filter": Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-31T23:55:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + }, + "WINDOW_0_SHORT_ABOVE_THRESHOLD": Object { + "bucket_script": Object { + "buckets_path": Object { + "burnRate": "WINDOW_0_SHORT_BURN_RATE", + }, + "script": Object { + "params": Object { + "threshold": 14.4, + }, + "source": "params.burnRate >= params.threshold ? 1 : 0", + }, + }, + }, + "WINDOW_0_SHORT_BURN_RATE": Object { + "bucket_script": Object { + "buckets_path": Object { + "good": "WINDOW_0_SHORT>good", + "total": "WINDOW_0_SHORT>total", + }, + "script": Object { + "params": Object { + "target": 0.999, + }, + "source": "params.total != null && params.total > 0 ? (1 - (params.good / params.total)) / (1 - params.target) : 0", + }, + }, + }, + "WINDOW_1_LONG": Object { + "aggs": Object { + "good": Object { + "sum": Object { + "field": "slo.numerator", + }, + }, + "total": Object { + "sum": Object { + "field": "slo.denominator", + }, + }, + }, + "filter": Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-31T18:00:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + }, + "WINDOW_1_LONG_ABOVE_THRESHOLD": Object { + "bucket_script": Object { + "buckets_path": Object { + "burnRate": "WINDOW_1_LONG_BURN_RATE", + }, + "script": Object { + "params": Object { + "threshold": 6, + }, + "source": "params.burnRate >= params.threshold ? 1 : 0", + }, + }, + }, + "WINDOW_1_LONG_BURN_RATE": Object { + "bucket_script": Object { + "buckets_path": Object { + "good": "WINDOW_1_LONG>good", + "total": "WINDOW_1_LONG>total", + }, + "script": Object { + "params": Object { + "target": 0.999, + }, + "source": "params.total != null && params.total > 0 ? (1 - (params.good / params.total)) / (1 - params.target) : 0", + }, + }, + }, + "WINDOW_1_SHORT": Object { + "aggs": Object { + "good": Object { + "sum": Object { + "field": "slo.numerator", + }, + }, + "total": Object { + "sum": Object { + "field": "slo.denominator", + }, + }, + }, + "filter": Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-31T23:30:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + }, + "WINDOW_1_SHORT_ABOVE_THRESHOLD": Object { + "bucket_script": Object { + "buckets_path": Object { + "burnRate": "WINDOW_1_SHORT_BURN_RATE", + }, + "script": Object { + "params": Object { + "threshold": 6, + }, + "source": "params.burnRate >= params.threshold ? 1 : 0", + }, + }, + }, + "WINDOW_1_SHORT_BURN_RATE": Object { + "bucket_script": Object { + "buckets_path": Object { + "good": "WINDOW_1_SHORT>good", + "total": "WINDOW_1_SHORT>total", + }, + "script": Object { + "params": Object { + "target": 0.999, + }, + "source": "params.total != null && params.total > 0 ? (1 - (params.good / params.total)) / (1 - params.target) : 0", + }, + }, + }, + "WINDOW_2_LONG": Object { + "aggs": Object { + "good": Object { + "sum": Object { + "field": "slo.numerator", + }, + }, + "total": Object { + "sum": Object { + "field": "slo.denominator", + }, + }, + }, + "filter": Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-31T00:00:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + }, + "WINDOW_2_LONG_ABOVE_THRESHOLD": Object { + "bucket_script": Object { + "buckets_path": Object { + "burnRate": "WINDOW_2_LONG_BURN_RATE", + }, + "script": Object { + "params": Object { + "threshold": 3, + }, + "source": "params.burnRate >= params.threshold ? 1 : 0", + }, + }, + }, + "WINDOW_2_LONG_BURN_RATE": Object { + "bucket_script": Object { + "buckets_path": Object { + "good": "WINDOW_2_LONG>good", + "total": "WINDOW_2_LONG>total", + }, + "script": Object { + "params": Object { + "target": 0.999, + }, + "source": "params.total != null && params.total > 0 ? (1 - (params.good / params.total)) / (1 - params.target) : 0", + }, + }, + }, + "WINDOW_2_SHORT": Object { + "aggs": Object { + "good": Object { + "sum": Object { + "field": "slo.numerator", + }, + }, + "total": Object { + "sum": Object { + "field": "slo.denominator", + }, + }, + }, + "filter": Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-31T22:00:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + }, + "WINDOW_2_SHORT_ABOVE_THRESHOLD": Object { + "bucket_script": Object { + "buckets_path": Object { + "burnRate": "WINDOW_2_SHORT_BURN_RATE", + }, + "script": Object { + "params": Object { + "threshold": 3, + }, + "source": "params.burnRate >= params.threshold ? 1 : 0", + }, + }, + }, + "WINDOW_2_SHORT_BURN_RATE": Object { + "bucket_script": Object { + "buckets_path": Object { + "good": "WINDOW_2_SHORT>good", + "total": "WINDOW_2_SHORT>total", + }, + "script": Object { + "params": Object { + "target": 0.999, + }, + "source": "params.total != null && params.total > 0 ? (1 - (params.good / params.total)) / (1 - params.target) : 0", + }, + }, + }, + "WINDOW_3_LONG": Object { + "aggs": Object { + "good": Object { + "sum": Object { + "field": "slo.numerator", + }, + }, + "total": Object { + "sum": Object { + "field": "slo.denominator", + }, + }, + }, + "filter": Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-29T00:00:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + }, + "WINDOW_3_LONG_ABOVE_THRESHOLD": Object { + "bucket_script": Object { + "buckets_path": Object { + "burnRate": "WINDOW_3_LONG_BURN_RATE", + }, + "script": Object { + "params": Object { + "threshold": 1, + }, + "source": "params.burnRate >= params.threshold ? 1 : 0", + }, + }, + }, + "WINDOW_3_LONG_BURN_RATE": Object { + "bucket_script": Object { + "buckets_path": Object { + "good": "WINDOW_3_LONG>good", + "total": "WINDOW_3_LONG>total", + }, + "script": Object { + "params": Object { + "target": 0.999, + }, + "source": "params.total != null && params.total > 0 ? (1 - (params.good / params.total)) / (1 - params.target) : 0", + }, + }, + }, + "WINDOW_3_SHORT": Object { + "aggs": Object { + "good": Object { + "sum": Object { + "field": "slo.numerator", + }, + }, + "total": Object { + "sum": Object { + "field": "slo.denominator", + }, + }, + }, + "filter": Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-31T18:00:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + }, + "WINDOW_3_SHORT_ABOVE_THRESHOLD": Object { + "bucket_script": Object { + "buckets_path": Object { + "burnRate": "WINDOW_3_SHORT_BURN_RATE", + }, + "script": Object { + "params": Object { + "threshold": 1, + }, + "source": "params.burnRate >= params.threshold ? 1 : 0", + }, + }, + }, + "WINDOW_3_SHORT_BURN_RATE": Object { + "bucket_script": Object { + "buckets_path": Object { + "good": "WINDOW_3_SHORT>good", + "total": "WINDOW_3_SHORT>total", + }, + "script": Object { + "params": Object { + "target": 0.999, + }, + "source": "params.total != null && params.total > 0 ? (1 - (params.good / params.total)) / (1 - params.target) : 0", + }, + }, + }, + "evaluation": Object { + "bucket_selector": Object { + "buckets_path": Object { + "WINDOW_0_LONG_ABOVE_THRESHOLD": "WINDOW_0_LONG_ABOVE_THRESHOLD", + "WINDOW_0_SHORT_ABOVE_THRESHOLD": "WINDOW_0_SHORT_ABOVE_THRESHOLD", + "WINDOW_1_LONG_ABOVE_THRESHOLD": "WINDOW_1_LONG_ABOVE_THRESHOLD", + "WINDOW_1_SHORT_ABOVE_THRESHOLD": "WINDOW_1_SHORT_ABOVE_THRESHOLD", + "WINDOW_2_LONG_ABOVE_THRESHOLD": "WINDOW_2_LONG_ABOVE_THRESHOLD", + "WINDOW_2_SHORT_ABOVE_THRESHOLD": "WINDOW_2_SHORT_ABOVE_THRESHOLD", + "WINDOW_3_LONG_ABOVE_THRESHOLD": "WINDOW_3_LONG_ABOVE_THRESHOLD", + "WINDOW_3_SHORT_ABOVE_THRESHOLD": "WINDOW_3_SHORT_ABOVE_THRESHOLD", + }, + "script": Object { + "source": "(params.WINDOW_0_SHORT_ABOVE_THRESHOLD == 1 && params.WINDOW_0_LONG_ABOVE_THRESHOLD == 1) || (params.WINDOW_1_SHORT_ABOVE_THRESHOLD == 1 && params.WINDOW_1_LONG_ABOVE_THRESHOLD == 1) || (params.WINDOW_2_SHORT_ABOVE_THRESHOLD == 1 && params.WINDOW_2_LONG_ABOVE_THRESHOLD == 1) || (params.WINDOW_3_SHORT_ABOVE_THRESHOLD == 1 && params.WINDOW_3_LONG_ABOVE_THRESHOLD == 1)", + }, + }, + }, + }, + "composite": Object { + "size": 1000, + "sources": Array [ + Object { + "instanceId": Object { + "terms": Object { + "field": "slo.instanceId", + }, + }, + }, + ], + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "slo.id": "test-slo", + }, + }, + Object { + "term": Object { + "slo.revision": 1, + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-29T00:00:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + ], + }, + }, + "size": 0, +} +`; + +exports[`buildQuery() should return a valid query for timeslices 1`] = ` +Object { + "aggs": Object { + "instances": Object { + "aggs": Object { + "WINDOW_0_LONG": Object { + "aggs": Object { + "good": Object { + "sum": Object { + "field": "slo.isGoodSlice", + }, + }, + "total": Object { + "value_count": Object { + "field": "slo.isGoodSlice", + }, + }, + }, + "filter": Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-31T23:00:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + }, + "WINDOW_0_LONG_ABOVE_THRESHOLD": Object { + "bucket_script": Object { + "buckets_path": Object { + "burnRate": "WINDOW_0_LONG_BURN_RATE", + }, + "script": Object { + "params": Object { + "threshold": 14.4, + }, + "source": "params.burnRate >= params.threshold ? 1 : 0", + }, + }, + }, + "WINDOW_0_LONG_BURN_RATE": Object { + "bucket_script": Object { + "buckets_path": Object { + "good": "WINDOW_0_LONG>good", + "total": "WINDOW_0_LONG>total", + }, + "script": Object { + "params": Object { + "target": 0.999, + }, + "source": "params.total != null && params.total > 0 ? (1 - (params.good / params.total)) / (1 - params.target) : 0", + }, + }, + }, + "WINDOW_0_SHORT": Object { + "aggs": Object { + "good": Object { + "sum": Object { + "field": "slo.isGoodSlice", + }, + }, + "total": Object { + "value_count": Object { + "field": "slo.isGoodSlice", + }, + }, + }, + "filter": Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-31T23:55:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + }, + "WINDOW_0_SHORT_ABOVE_THRESHOLD": Object { + "bucket_script": Object { + "buckets_path": Object { + "burnRate": "WINDOW_0_SHORT_BURN_RATE", + }, + "script": Object { + "params": Object { + "threshold": 14.4, + }, + "source": "params.burnRate >= params.threshold ? 1 : 0", + }, + }, + }, + "WINDOW_0_SHORT_BURN_RATE": Object { + "bucket_script": Object { + "buckets_path": Object { + "good": "WINDOW_0_SHORT>good", + "total": "WINDOW_0_SHORT>total", + }, + "script": Object { + "params": Object { + "target": 0.999, + }, + "source": "params.total != null && params.total > 0 ? (1 - (params.good / params.total)) / (1 - params.target) : 0", + }, + }, + }, + "WINDOW_1_LONG": Object { + "aggs": Object { + "good": Object { + "sum": Object { + "field": "slo.isGoodSlice", + }, + }, + "total": Object { + "value_count": Object { + "field": "slo.isGoodSlice", + }, + }, + }, + "filter": Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-31T18:00:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + }, + "WINDOW_1_LONG_ABOVE_THRESHOLD": Object { + "bucket_script": Object { + "buckets_path": Object { + "burnRate": "WINDOW_1_LONG_BURN_RATE", + }, + "script": Object { + "params": Object { + "threshold": 6, + }, + "source": "params.burnRate >= params.threshold ? 1 : 0", + }, + }, + }, + "WINDOW_1_LONG_BURN_RATE": Object { + "bucket_script": Object { + "buckets_path": Object { + "good": "WINDOW_1_LONG>good", + "total": "WINDOW_1_LONG>total", + }, + "script": Object { + "params": Object { + "target": 0.999, + }, + "source": "params.total != null && params.total > 0 ? (1 - (params.good / params.total)) / (1 - params.target) : 0", + }, + }, + }, + "WINDOW_1_SHORT": Object { + "aggs": Object { + "good": Object { + "sum": Object { + "field": "slo.isGoodSlice", + }, + }, + "total": Object { + "value_count": Object { + "field": "slo.isGoodSlice", + }, + }, + }, + "filter": Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-31T23:30:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + }, + "WINDOW_1_SHORT_ABOVE_THRESHOLD": Object { + "bucket_script": Object { + "buckets_path": Object { + "burnRate": "WINDOW_1_SHORT_BURN_RATE", + }, + "script": Object { + "params": Object { + "threshold": 6, + }, + "source": "params.burnRate >= params.threshold ? 1 : 0", + }, + }, + }, + "WINDOW_1_SHORT_BURN_RATE": Object { + "bucket_script": Object { + "buckets_path": Object { + "good": "WINDOW_1_SHORT>good", + "total": "WINDOW_1_SHORT>total", + }, + "script": Object { + "params": Object { + "target": 0.999, + }, + "source": "params.total != null && params.total > 0 ? (1 - (params.good / params.total)) / (1 - params.target) : 0", + }, + }, + }, + "WINDOW_2_LONG": Object { + "aggs": Object { + "good": Object { + "sum": Object { + "field": "slo.isGoodSlice", + }, + }, + "total": Object { + "value_count": Object { + "field": "slo.isGoodSlice", + }, + }, + }, + "filter": Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-31T00:00:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + }, + "WINDOW_2_LONG_ABOVE_THRESHOLD": Object { + "bucket_script": Object { + "buckets_path": Object { + "burnRate": "WINDOW_2_LONG_BURN_RATE", + }, + "script": Object { + "params": Object { + "threshold": 3, + }, + "source": "params.burnRate >= params.threshold ? 1 : 0", + }, + }, + }, + "WINDOW_2_LONG_BURN_RATE": Object { + "bucket_script": Object { + "buckets_path": Object { + "good": "WINDOW_2_LONG>good", + "total": "WINDOW_2_LONG>total", + }, + "script": Object { + "params": Object { + "target": 0.999, + }, + "source": "params.total != null && params.total > 0 ? (1 - (params.good / params.total)) / (1 - params.target) : 0", + }, + }, + }, + "WINDOW_2_SHORT": Object { + "aggs": Object { + "good": Object { + "sum": Object { + "field": "slo.isGoodSlice", + }, + }, + "total": Object { + "value_count": Object { + "field": "slo.isGoodSlice", + }, + }, + }, + "filter": Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-31T22:00:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + }, + "WINDOW_2_SHORT_ABOVE_THRESHOLD": Object { + "bucket_script": Object { + "buckets_path": Object { + "burnRate": "WINDOW_2_SHORT_BURN_RATE", + }, + "script": Object { + "params": Object { + "threshold": 3, + }, + "source": "params.burnRate >= params.threshold ? 1 : 0", + }, + }, + }, + "WINDOW_2_SHORT_BURN_RATE": Object { + "bucket_script": Object { + "buckets_path": Object { + "good": "WINDOW_2_SHORT>good", + "total": "WINDOW_2_SHORT>total", + }, + "script": Object { + "params": Object { + "target": 0.999, + }, + "source": "params.total != null && params.total > 0 ? (1 - (params.good / params.total)) / (1 - params.target) : 0", + }, + }, + }, + "WINDOW_3_LONG": Object { + "aggs": Object { + "good": Object { + "sum": Object { + "field": "slo.isGoodSlice", + }, + }, + "total": Object { + "value_count": Object { + "field": "slo.isGoodSlice", + }, + }, + }, + "filter": Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-29T00:00:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + }, + "WINDOW_3_LONG_ABOVE_THRESHOLD": Object { + "bucket_script": Object { + "buckets_path": Object { + "burnRate": "WINDOW_3_LONG_BURN_RATE", + }, + "script": Object { + "params": Object { + "threshold": 1, + }, + "source": "params.burnRate >= params.threshold ? 1 : 0", + }, + }, + }, + "WINDOW_3_LONG_BURN_RATE": Object { + "bucket_script": Object { + "buckets_path": Object { + "good": "WINDOW_3_LONG>good", + "total": "WINDOW_3_LONG>total", + }, + "script": Object { + "params": Object { + "target": 0.999, + }, + "source": "params.total != null && params.total > 0 ? (1 - (params.good / params.total)) / (1 - params.target) : 0", + }, + }, + }, + "WINDOW_3_SHORT": Object { + "aggs": Object { + "good": Object { + "sum": Object { + "field": "slo.isGoodSlice", + }, + }, + "total": Object { + "value_count": Object { + "field": "slo.isGoodSlice", + }, + }, + }, + "filter": Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-31T18:00:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + }, + "WINDOW_3_SHORT_ABOVE_THRESHOLD": Object { + "bucket_script": Object { + "buckets_path": Object { + "burnRate": "WINDOW_3_SHORT_BURN_RATE", + }, + "script": Object { + "params": Object { + "threshold": 1, + }, + "source": "params.burnRate >= params.threshold ? 1 : 0", + }, + }, + }, + "WINDOW_3_SHORT_BURN_RATE": Object { + "bucket_script": Object { + "buckets_path": Object { + "good": "WINDOW_3_SHORT>good", + "total": "WINDOW_3_SHORT>total", + }, + "script": Object { + "params": Object { + "target": 0.999, + }, + "source": "params.total != null && params.total > 0 ? (1 - (params.good / params.total)) / (1 - params.target) : 0", + }, + }, + }, + "evaluation": Object { + "bucket_selector": Object { + "buckets_path": Object { + "WINDOW_0_LONG_ABOVE_THRESHOLD": "WINDOW_0_LONG_ABOVE_THRESHOLD", + "WINDOW_0_SHORT_ABOVE_THRESHOLD": "WINDOW_0_SHORT_ABOVE_THRESHOLD", + "WINDOW_1_LONG_ABOVE_THRESHOLD": "WINDOW_1_LONG_ABOVE_THRESHOLD", + "WINDOW_1_SHORT_ABOVE_THRESHOLD": "WINDOW_1_SHORT_ABOVE_THRESHOLD", + "WINDOW_2_LONG_ABOVE_THRESHOLD": "WINDOW_2_LONG_ABOVE_THRESHOLD", + "WINDOW_2_SHORT_ABOVE_THRESHOLD": "WINDOW_2_SHORT_ABOVE_THRESHOLD", + "WINDOW_3_LONG_ABOVE_THRESHOLD": "WINDOW_3_LONG_ABOVE_THRESHOLD", + "WINDOW_3_SHORT_ABOVE_THRESHOLD": "WINDOW_3_SHORT_ABOVE_THRESHOLD", + }, + "script": Object { + "source": "(params.WINDOW_0_SHORT_ABOVE_THRESHOLD == 1 && params.WINDOW_0_LONG_ABOVE_THRESHOLD == 1) || (params.WINDOW_1_SHORT_ABOVE_THRESHOLD == 1 && params.WINDOW_1_LONG_ABOVE_THRESHOLD == 1) || (params.WINDOW_2_SHORT_ABOVE_THRESHOLD == 1 && params.WINDOW_2_LONG_ABOVE_THRESHOLD == 1) || (params.WINDOW_3_SHORT_ABOVE_THRESHOLD == 1 && params.WINDOW_3_LONG_ABOVE_THRESHOLD == 1)", + }, + }, + }, + }, + "composite": Object { + "size": 1000, + "sources": Array [ + Object { + "instanceId": Object { + "terms": Object { + "field": "slo.instanceId", + }, + }, + }, + ], + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "slo.id": "test-slo", + }, + }, + Object { + "term": Object { + "slo.revision": 1, + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-29T00:00:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + ], + }, + }, + "size": 0, +} +`; + +exports[`buildQuery() should return a valid query with afterKey 1`] = ` +Object { + "aggs": Object { + "instances": Object { + "aggs": Object { + "WINDOW_0_LONG": Object { + "aggs": Object { + "good": Object { + "sum": Object { + "field": "slo.numerator", + }, + }, + "total": Object { + "sum": Object { + "field": "slo.denominator", + }, + }, + }, + "filter": Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-31T23:00:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + }, + "WINDOW_0_LONG_ABOVE_THRESHOLD": Object { + "bucket_script": Object { + "buckets_path": Object { + "burnRate": "WINDOW_0_LONG_BURN_RATE", + }, + "script": Object { + "params": Object { + "threshold": 14.4, + }, + "source": "params.burnRate >= params.threshold ? 1 : 0", + }, + }, + }, + "WINDOW_0_LONG_BURN_RATE": Object { + "bucket_script": Object { + "buckets_path": Object { + "good": "WINDOW_0_LONG>good", + "total": "WINDOW_0_LONG>total", + }, + "script": Object { + "params": Object { + "target": 0.999, + }, + "source": "params.total != null && params.total > 0 ? (1 - (params.good / params.total)) / (1 - params.target) : 0", + }, + }, + }, + "WINDOW_0_SHORT": Object { + "aggs": Object { + "good": Object { + "sum": Object { + "field": "slo.numerator", + }, + }, + "total": Object { + "sum": Object { + "field": "slo.denominator", + }, + }, + }, + "filter": Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-31T23:55:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + }, + "WINDOW_0_SHORT_ABOVE_THRESHOLD": Object { + "bucket_script": Object { + "buckets_path": Object { + "burnRate": "WINDOW_0_SHORT_BURN_RATE", + }, + "script": Object { + "params": Object { + "threshold": 14.4, + }, + "source": "params.burnRate >= params.threshold ? 1 : 0", + }, + }, + }, + "WINDOW_0_SHORT_BURN_RATE": Object { + "bucket_script": Object { + "buckets_path": Object { + "good": "WINDOW_0_SHORT>good", + "total": "WINDOW_0_SHORT>total", + }, + "script": Object { + "params": Object { + "target": 0.999, + }, + "source": "params.total != null && params.total > 0 ? (1 - (params.good / params.total)) / (1 - params.target) : 0", + }, + }, + }, + "WINDOW_1_LONG": Object { + "aggs": Object { + "good": Object { + "sum": Object { + "field": "slo.numerator", + }, + }, + "total": Object { + "sum": Object { + "field": "slo.denominator", + }, + }, + }, + "filter": Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-31T18:00:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + }, + "WINDOW_1_LONG_ABOVE_THRESHOLD": Object { + "bucket_script": Object { + "buckets_path": Object { + "burnRate": "WINDOW_1_LONG_BURN_RATE", + }, + "script": Object { + "params": Object { + "threshold": 6, + }, + "source": "params.burnRate >= params.threshold ? 1 : 0", + }, + }, + }, + "WINDOW_1_LONG_BURN_RATE": Object { + "bucket_script": Object { + "buckets_path": Object { + "good": "WINDOW_1_LONG>good", + "total": "WINDOW_1_LONG>total", + }, + "script": Object { + "params": Object { + "target": 0.999, + }, + "source": "params.total != null && params.total > 0 ? (1 - (params.good / params.total)) / (1 - params.target) : 0", + }, + }, + }, + "WINDOW_1_SHORT": Object { + "aggs": Object { + "good": Object { + "sum": Object { + "field": "slo.numerator", + }, + }, + "total": Object { + "sum": Object { + "field": "slo.denominator", + }, + }, + }, + "filter": Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-31T23:30:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + }, + "WINDOW_1_SHORT_ABOVE_THRESHOLD": Object { + "bucket_script": Object { + "buckets_path": Object { + "burnRate": "WINDOW_1_SHORT_BURN_RATE", + }, + "script": Object { + "params": Object { + "threshold": 6, + }, + "source": "params.burnRate >= params.threshold ? 1 : 0", + }, + }, + }, + "WINDOW_1_SHORT_BURN_RATE": Object { + "bucket_script": Object { + "buckets_path": Object { + "good": "WINDOW_1_SHORT>good", + "total": "WINDOW_1_SHORT>total", + }, + "script": Object { + "params": Object { + "target": 0.999, + }, + "source": "params.total != null && params.total > 0 ? (1 - (params.good / params.total)) / (1 - params.target) : 0", + }, + }, + }, + "WINDOW_2_LONG": Object { + "aggs": Object { + "good": Object { + "sum": Object { + "field": "slo.numerator", + }, + }, + "total": Object { + "sum": Object { + "field": "slo.denominator", + }, + }, + }, + "filter": Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-31T00:00:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + }, + "WINDOW_2_LONG_ABOVE_THRESHOLD": Object { + "bucket_script": Object { + "buckets_path": Object { + "burnRate": "WINDOW_2_LONG_BURN_RATE", + }, + "script": Object { + "params": Object { + "threshold": 3, + }, + "source": "params.burnRate >= params.threshold ? 1 : 0", + }, + }, + }, + "WINDOW_2_LONG_BURN_RATE": Object { + "bucket_script": Object { + "buckets_path": Object { + "good": "WINDOW_2_LONG>good", + "total": "WINDOW_2_LONG>total", + }, + "script": Object { + "params": Object { + "target": 0.999, + }, + "source": "params.total != null && params.total > 0 ? (1 - (params.good / params.total)) / (1 - params.target) : 0", + }, + }, + }, + "WINDOW_2_SHORT": Object { + "aggs": Object { + "good": Object { + "sum": Object { + "field": "slo.numerator", + }, + }, + "total": Object { + "sum": Object { + "field": "slo.denominator", + }, + }, + }, + "filter": Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-31T22:00:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + }, + "WINDOW_2_SHORT_ABOVE_THRESHOLD": Object { + "bucket_script": Object { + "buckets_path": Object { + "burnRate": "WINDOW_2_SHORT_BURN_RATE", + }, + "script": Object { + "params": Object { + "threshold": 3, + }, + "source": "params.burnRate >= params.threshold ? 1 : 0", + }, + }, + }, + "WINDOW_2_SHORT_BURN_RATE": Object { + "bucket_script": Object { + "buckets_path": Object { + "good": "WINDOW_2_SHORT>good", + "total": "WINDOW_2_SHORT>total", + }, + "script": Object { + "params": Object { + "target": 0.999, + }, + "source": "params.total != null && params.total > 0 ? (1 - (params.good / params.total)) / (1 - params.target) : 0", + }, + }, + }, + "WINDOW_3_LONG": Object { + "aggs": Object { + "good": Object { + "sum": Object { + "field": "slo.numerator", + }, + }, + "total": Object { + "sum": Object { + "field": "slo.denominator", + }, + }, + }, + "filter": Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-29T00:00:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + }, + "WINDOW_3_LONG_ABOVE_THRESHOLD": Object { + "bucket_script": Object { + "buckets_path": Object { + "burnRate": "WINDOW_3_LONG_BURN_RATE", + }, + "script": Object { + "params": Object { + "threshold": 1, + }, + "source": "params.burnRate >= params.threshold ? 1 : 0", + }, + }, + }, + "WINDOW_3_LONG_BURN_RATE": Object { + "bucket_script": Object { + "buckets_path": Object { + "good": "WINDOW_3_LONG>good", + "total": "WINDOW_3_LONG>total", + }, + "script": Object { + "params": Object { + "target": 0.999, + }, + "source": "params.total != null && params.total > 0 ? (1 - (params.good / params.total)) / (1 - params.target) : 0", + }, + }, + }, + "WINDOW_3_SHORT": Object { + "aggs": Object { + "good": Object { + "sum": Object { + "field": "slo.numerator", + }, + }, + "total": Object { + "sum": Object { + "field": "slo.denominator", + }, + }, + }, + "filter": Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-31T18:00:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + }, + "WINDOW_3_SHORT_ABOVE_THRESHOLD": Object { + "bucket_script": Object { + "buckets_path": Object { + "burnRate": "WINDOW_3_SHORT_BURN_RATE", + }, + "script": Object { + "params": Object { + "threshold": 1, + }, + "source": "params.burnRate >= params.threshold ? 1 : 0", + }, + }, + }, + "WINDOW_3_SHORT_BURN_RATE": Object { + "bucket_script": Object { + "buckets_path": Object { + "good": "WINDOW_3_SHORT>good", + "total": "WINDOW_3_SHORT>total", + }, + "script": Object { + "params": Object { + "target": 0.999, + }, + "source": "params.total != null && params.total > 0 ? (1 - (params.good / params.total)) / (1 - params.target) : 0", + }, + }, + }, + "evaluation": Object { + "bucket_selector": Object { + "buckets_path": Object { + "WINDOW_0_LONG_ABOVE_THRESHOLD": "WINDOW_0_LONG_ABOVE_THRESHOLD", + "WINDOW_0_SHORT_ABOVE_THRESHOLD": "WINDOW_0_SHORT_ABOVE_THRESHOLD", + "WINDOW_1_LONG_ABOVE_THRESHOLD": "WINDOW_1_LONG_ABOVE_THRESHOLD", + "WINDOW_1_SHORT_ABOVE_THRESHOLD": "WINDOW_1_SHORT_ABOVE_THRESHOLD", + "WINDOW_2_LONG_ABOVE_THRESHOLD": "WINDOW_2_LONG_ABOVE_THRESHOLD", + "WINDOW_2_SHORT_ABOVE_THRESHOLD": "WINDOW_2_SHORT_ABOVE_THRESHOLD", + "WINDOW_3_LONG_ABOVE_THRESHOLD": "WINDOW_3_LONG_ABOVE_THRESHOLD", + "WINDOW_3_SHORT_ABOVE_THRESHOLD": "WINDOW_3_SHORT_ABOVE_THRESHOLD", + }, + "script": Object { + "source": "(params.WINDOW_0_SHORT_ABOVE_THRESHOLD == 1 && params.WINDOW_0_LONG_ABOVE_THRESHOLD == 1) || (params.WINDOW_1_SHORT_ABOVE_THRESHOLD == 1 && params.WINDOW_1_LONG_ABOVE_THRESHOLD == 1) || (params.WINDOW_2_SHORT_ABOVE_THRESHOLD == 1 && params.WINDOW_2_LONG_ABOVE_THRESHOLD == 1) || (params.WINDOW_3_SHORT_ABOVE_THRESHOLD == 1 && params.WINDOW_3_LONG_ABOVE_THRESHOLD == 1)", + }, + }, + }, + }, + "composite": Object { + "after": Object { + "instanceId": "example", + }, + "size": 1000, + "sources": Array [ + Object { + "instanceId": Object { + "terms": Object { + "field": "slo.instanceId", + }, + }, + }, + ], + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "slo.id": "test-slo", + }, + }, + Object { + "term": Object { + "slo.revision": 1, + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "gte": "2022-12-29T00:00:00.000Z", + "lt": "2023-01-01T00:00:00.000Z", + }, + }, + }, + ], + }, + }, + "size": 0, +} +`; diff --git a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/lib/build_query.test.ts b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/lib/build_query.test.ts new file mode 100644 index 0000000000000..730fe8ae66e46 --- /dev/null +++ b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/lib/build_query.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createBurnRateRule } from '../fixtures/rule'; +import { buildQuery } from './build_query'; +import { createKQLCustomIndicator, createSLO } from '../../../../services/slo/fixtures/slo'; + +const STARTED_AT = new Date('2023-01-01T00:00:00.000Z'); + +describe('buildQuery()', () => { + it('should return a valid query for occurrences', () => { + const slo = createSLO({ + id: 'test-slo', + indicator: createKQLCustomIndicator(), + }); + const rule = createBurnRateRule(slo); + expect(buildQuery(STARTED_AT, slo, rule)).toMatchSnapshot(); + }); + it('should return a valid query with afterKey', () => { + const slo = createSLO({ + id: 'test-slo', + indicator: createKQLCustomIndicator(), + }); + const rule = createBurnRateRule(slo); + expect(buildQuery(STARTED_AT, slo, rule, { instanceId: 'example' })).toMatchSnapshot(); + }); + it('should return a valid query for timeslices', () => { + const slo = createSLO({ + id: 'test-slo', + indicator: createKQLCustomIndicator(), + budgetingMethod: 'timeslices', + }); + const rule = createBurnRateRule(slo); + expect(buildQuery(STARTED_AT, slo, rule)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/lib/build_query.ts b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/lib/build_query.ts new file mode 100644 index 0000000000000..8d5bfe795aa08 --- /dev/null +++ b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/lib/build_query.ts @@ -0,0 +1,218 @@ +/* + * 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 moment from 'moment'; +import { timeslicesBudgetingMethodSchema } from '@kbn/slo-schema'; +import { Duration, SLO, toDurationUnit, toMomentUnitOfTime } from '../../../../domain/models'; +import { BurnRateRuleParams, WindowSchema } from '../types'; + +type BurnRateWindowWithDuration = WindowSchema & { + longDuration: Duration; + shortDuration: Duration; +}; + +export interface EvaluationAfterKey { + instanceId: string; +} + +export const LONG_WINDOW = 'LONG'; +export const SHORT_WINDOW = 'SHORT'; +const BURN_RATE = 'BURN_RATE'; +const WINDOW = 'WINDOW'; +const ABOVE_THRESHOLD = 'ABOVE_THRESHOLD'; +type WindowType = typeof LONG_WINDOW | typeof SHORT_WINDOW; + +export function generateWindowId(index: string | number) { + return `${WINDOW}_${index}`; +} + +export function generateStatsKey(id: string, type: WindowType) { + return `${id}_${type}`; +} + +export function generateBurnRateKey(id: string, type: WindowType) { + return `${generateStatsKey(id, type)}_${BURN_RATE}`; +} + +export function generateAboveThresholdKey(id: string, type: WindowType) { + return `${generateStatsKey(id, type)}_${ABOVE_THRESHOLD}`; +} + +const TIMESLICE_AGGS = { + good: { sum: { field: 'slo.isGoodSlice' } }, + total: { value_count: { field: 'slo.isGoodSlice' } }, +}; +const OCCURRENCE_AGGS = { + good: { sum: { field: 'slo.numerator' } }, + total: { sum: { field: 'slo.denominator' } }, +}; + +function buildWindowAgg( + id: string, + type: WindowType, + threshold: number, + slo: SLO, + dateRange: { from: Date; to: Date } +) { + const aggs = timeslicesBudgetingMethodSchema.is(slo.budgetingMethod) + ? TIMESLICE_AGGS + : OCCURRENCE_AGGS; + + return { + [`${id}_${type}`]: { + filter: { + range: { + '@timestamp': { + gte: dateRange.from.toISOString(), + lt: dateRange.to.toISOString(), + }, + }, + }, + aggs, + }, + [generateBurnRateKey(id, type)]: { + bucket_script: { + buckets_path: { + good: `${id}_${type}>good`, + total: `${id}_${type}>total`, + }, + script: { + source: + 'params.total != null && params.total > 0 ? (1 - (params.good / params.total)) / (1 - params.target) : 0', + params: { target: slo.objective.target }, + }, + }, + }, + [generateAboveThresholdKey(id, type)]: { + bucket_script: { + buckets_path: { burnRate: generateBurnRateKey(id, type) }, + script: { + source: 'params.burnRate >= params.threshold ? 1 : 0', + params: { threshold }, + }, + }, + }, + }; +} + +function buildWindowAggs(startedAt: Date, slo: SLO, burnRateWindows: BurnRateWindowWithDuration[]) { + return burnRateWindows.reduce((acc, winDef, index) => { + const shortDateRange = getLookbackDateRange(startedAt, winDef.shortDuration); + const longDateRange = getLookbackDateRange(startedAt, winDef.longDuration); + const windowId = generateWindowId(index); + return { + ...acc, + ...buildWindowAgg(windowId, SHORT_WINDOW, winDef.burnRateThreshold, slo, shortDateRange), + ...buildWindowAgg(windowId, LONG_WINDOW, winDef.burnRateThreshold, slo, longDateRange), + }; + }, {}); +} + +function buildEvaluation(burnRateWindows: BurnRateWindowWithDuration[]) { + const bucketsPath = burnRateWindows.reduce((acc, _winDef, index) => { + const windowId = `${WINDOW}_${index}`; + return { + ...acc, + [generateAboveThresholdKey(windowId, SHORT_WINDOW)]: generateAboveThresholdKey( + windowId, + SHORT_WINDOW + ), + [generateAboveThresholdKey(windowId, LONG_WINDOW)]: generateAboveThresholdKey( + windowId, + LONG_WINDOW + ), + }; + }, {}); + + const source = burnRateWindows.reduce((acc, _windDef, index) => { + const windowId = `${WINDOW}_${index}`; + const OP = acc ? ' || ' : ''; + return `${acc}${OP}(params.${generateAboveThresholdKey( + windowId, + SHORT_WINDOW + )} == 1 && params.${generateAboveThresholdKey(windowId, LONG_WINDOW)} == 1)`; + }, ''); + + return { + evaluation: { + bucket_selector: { + buckets_path: bucketsPath, + script: { + source, + }, + }, + }, + }; +} + +export function buildQuery( + startedAt: Date, + slo: SLO, + params: BurnRateRuleParams, + afterKey?: EvaluationAfterKey +) { + const burnRateWindows = params.windows.map((winDef) => { + return { + ...winDef, + longDuration: new Duration(winDef.longWindow.value, toDurationUnit(winDef.longWindow.unit)), + shortDuration: new Duration( + winDef.shortWindow.value, + toDurationUnit(winDef.shortWindow.unit) + ), + }; + }); + + const longestLookbackWindow = burnRateWindows.reduce((acc, winDef) => { + return winDef.longDuration.isShorterThan(acc.longDuration) ? acc : winDef; + }, burnRateWindows[0]); + const longestDateRange = getLookbackDateRange(startedAt, longestLookbackWindow.longDuration); + + return { + size: 0, + query: { + bool: { + filter: [ + { term: { 'slo.id': slo.id } }, + { term: { 'slo.revision': slo.revision } }, + { + range: { + '@timestamp': { + gte: longestDateRange.from.toISOString(), + lt: longestDateRange.to.toISOString(), + }, + }, + }, + ], + }, + }, + aggs: { + instances: { + composite: { + ...(afterKey ? { after: afterKey } : {}), + size: 1000, + sources: [{ instanceId: { terms: { field: 'slo.instanceId' } } }], + }, + aggs: { + ...buildWindowAggs(startedAt, slo, burnRateWindows), + ...buildEvaluation(burnRateWindows), + }, + }, + }, + }; +} + +function getLookbackDateRange(startedAt: Date, duration: Duration): { from: Date; to: Date } { + const unit = toMomentUnitOfTime(duration.unit); + const now = moment(startedAt).startOf('minute'); + const from = now.clone().subtract(duration.value, unit); + const to = now.clone(); + + return { + from: from.toDate(), + to: to.toDate(), + }; +} diff --git a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/lib/evaluate.ts b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/lib/evaluate.ts new file mode 100644 index 0000000000000..8461382fc1564 --- /dev/null +++ b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/lib/evaluate.ts @@ -0,0 +1,144 @@ +/* + * 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 { ElasticsearchClient } from '@kbn/core/server'; +import { get } from 'lodash'; +import { Duration, SLO, toDurationUnit } from '../../../../domain/models'; +import { BurnRateRuleParams } from '../types'; +import { SLO_DESTINATION_INDEX_PATTERN } from '../../../../assets/constants'; +import { + buildQuery, + EvaluationAfterKey, + generateAboveThresholdKey, + generateBurnRateKey, + generateWindowId, + LONG_WINDOW, + SHORT_WINDOW, +} from './build_query'; + +export interface EvaluationWindowStats { + doc_count: number; + good: { value: number }; + total: { value: number }; +} + +export interface EvaluationBucket { + key: EvaluationAfterKey; + doc_count: number; + WINDOW_0_SHORT?: EvaluationWindowStats; + WINDOW_1_SHORT?: EvaluationWindowStats; + WINDOW_2_SHORT?: EvaluationWindowStats; + WINDOW_3_SHORT?: EvaluationWindowStats; + WINDOW_0_LONG?: EvaluationWindowStats; + WINDOW_1_LONG?: EvaluationWindowStats; + WINDOW_2_LONG?: EvaluationWindowStats; + WINDOW_3_LONG?: EvaluationWindowStats; + WINDOW_0_SHORT_BURN_RATE?: { value: number }; + WINDOW_1_SHORT_BURN_RATE?: { value: number }; + WINDOW_2_SHORT_BURN_RATE?: { value: number }; + WINDOW_3_SHORT_BURN_RATE?: { value: number }; + WINDOW_0_LONG_BURN_RATE?: { value: number }; + WINDOW_1_LONG_BURN_RATE?: { value: number }; + WINDOW_2_LONG_BURN_RATE?: { value: number }; + WINDOW_3_LONG_BURN_RATE?: { value: number }; + WINDOW_0_SHORT_ABOVE_THRESHOLD?: { value: number }; + WINDOW_1_SHORT_ABOVE_THRESHOLD?: { value: number }; + WINDOW_2_SHORT_ABOVE_THRESHOLD?: { value: number }; + WINDOW_3_SHORT_ABOVE_THRESHOLD?: { value: number }; + WINDOW_0_LONG_ABOVE_THRESHOLD?: { value: number }; + WINDOW_1_LONG_ABOVE_THRESHOLD?: { value: number }; + WINDOW_2_LONG_ABOVE_THRESHOLD?: { value: number }; + WINDOW_3_LONG_ABOVE_THRESHOLD?: { value: number }; +} + +export interface EvalutionAggResults { + instances: { + after_key?: EvaluationAfterKey; + buckets: EvaluationBucket[]; + }; +} + +async function queryAllResults( + esClient: ElasticsearchClient, + slo: SLO, + params: BurnRateRuleParams, + startedAt: Date, + buckets: EvaluationBucket[] = [], + lastAfterKey?: { instanceId: string } +): Promise { + const queryAndAggs = buildQuery(startedAt, slo, params, lastAfterKey); + const results = await esClient.search({ + index: SLO_DESTINATION_INDEX_PATTERN, + ...queryAndAggs, + }); + if (!results.aggregations) { + throw new Error('Elasticsearch query failed to return a valid aggregation'); + } + if (results.aggregations.instances.buckets.length === 0) { + return buckets; + } + return queryAllResults( + esClient, + slo, + params, + startedAt, + [...buckets, ...results.aggregations.instances.buckets], + results.aggregations.instances.after_key + ); +} + +export async function evaluate( + esClient: ElasticsearchClient, + slo: SLO, + params: BurnRateRuleParams, + startedAt: Date +) { + const buckets = await queryAllResults(esClient, slo, params, startedAt); + return transformBucketToResults(buckets, params); +} + +function transformBucketToResults(buckets: EvaluationBucket[], params: BurnRateRuleParams) { + return buckets.map((bucket) => { + for (const index in params.windows) { + if (params.windows[index]) { + const winDef = params.windows[index]; + const windowId = generateWindowId(index); + const shortWindowThresholdKey = generateAboveThresholdKey(windowId, SHORT_WINDOW); + const longWindowThresholdKey = generateAboveThresholdKey(windowId, LONG_WINDOW); + const isShortWindowTriggering = get(bucket, [shortWindowThresholdKey, 'value'], 0); + const isLongWindowTriggering = get(bucket, [longWindowThresholdKey, 'value'], 0); + + if (isShortWindowTriggering && isLongWindowTriggering) { + return { + instanceId: bucket.key.instanceId, + shouldAlert: true, + longWindowBurnRate: get( + bucket, + [generateBurnRateKey(windowId, LONG_WINDOW), 'value'], + 0 + ) as number, + shortWindowBurnRate: get( + bucket, + [generateBurnRateKey(windowId, SHORT_WINDOW), 'value'], + 0 + ) as number, + shortWindowDuration: new Duration( + winDef.shortWindow.value, + toDurationUnit(winDef.shortWindow.unit) + ), + longWindowDuration: new Duration( + winDef.longWindow.value, + toDurationUnit(winDef.longWindow.unit) + ), + window: winDef, + }; + } + } + } + throw new Error(`Evaluation query for ${bucket.key.instanceId} failed.`); + }); +} diff --git a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/register.ts b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/register.ts index 146e4682c105d..8bff7173764df 100644 --- a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/register.ts +++ b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/register.ts @@ -76,6 +76,7 @@ export function sloBurnRateRuleType( { name: 'alertDetailsUrl', description: alertDetailsUrlActionVariableDescription }, { name: 'sloId', description: sloIdActionVariableDescription }, { name: 'sloName', description: sloNameActionVariableDescription }, + { name: 'sloInstanceId', description: sloInstanceIdActionVariableDescription }, ], }, alerts: { @@ -142,3 +143,10 @@ export const sloNameActionVariableDescription = i18n.translate( defaultMessage: 'The SLO name.', } ); + +export const sloInstanceIdActionVariableDescription = i18n.translate( + 'xpack.observability.slo.alerting.sloInstanceIdDescription', + { + defaultMessage: 'The SLO instance id.', + } +); diff --git a/x-pack/plugins/observability/server/routes/slo/route.ts b/x-pack/plugins/observability/server/routes/slo/route.ts index fdcdde0197a04..14e5a26e7c7ff 100644 --- a/x-pack/plugins/observability/server/routes/slo/route.ts +++ b/x-pack/plugins/observability/server/routes/slo/route.ts @@ -65,7 +65,7 @@ const isLicenseAtLeastPlatinum = async (context: ObservabilityRequestHandlerCont const createSLORoute = createObservabilityServerRoute({ endpoint: 'POST /api/observability/slos 2023-10-31', options: { - tags: ['access:public', 'access:slo_write'], + tags: ['access:slo_write'], }, params: createSLOParamsSchema, handler: async ({ context, params, logger }) => { @@ -90,7 +90,7 @@ const createSLORoute = createObservabilityServerRoute({ const updateSLORoute = createObservabilityServerRoute({ endpoint: 'PUT /api/observability/slos/{id} 2023-10-31', options: { - tags: ['access:public', 'access:slo_write'], + tags: ['access:slo_write'], }, params: updateSLOParamsSchema, handler: async ({ context, params, logger }) => { @@ -116,7 +116,7 @@ const updateSLORoute = createObservabilityServerRoute({ const deleteSLORoute = createObservabilityServerRoute({ endpoint: 'DELETE /api/observability/slos/{id} 2023-10-31', options: { - tags: ['access:public', 'access:slo_write'], + tags: ['access:slo_write'], }, params: deleteSLOParamsSchema, handler: async ({ @@ -148,7 +148,7 @@ const deleteSLORoute = createObservabilityServerRoute({ const getSLORoute = createObservabilityServerRoute({ endpoint: 'GET /api/observability/slos/{id} 2023-10-31', options: { - tags: ['access:public', 'access:slo_read'], + tags: ['access:slo_read'], }, params: getSLOParamsSchema, handler: async ({ context, params }) => { @@ -173,7 +173,7 @@ const getSLORoute = createObservabilityServerRoute({ const enableSLORoute = createObservabilityServerRoute({ endpoint: 'POST /api/observability/slos/{id}/enable 2023-10-31', options: { - tags: ['access:public', 'access:slo_write'], + tags: ['access:slo_write'], }, params: manageSLOParamsSchema, handler: async ({ context, params, logger }) => { @@ -199,7 +199,7 @@ const enableSLORoute = createObservabilityServerRoute({ const disableSLORoute = createObservabilityServerRoute({ endpoint: 'POST /api/observability/slos/{id}/disable 2023-10-31', options: { - tags: ['access:public', 'access:slo_write'], + tags: ['access:slo_write'], }, params: manageSLOParamsSchema, handler: async ({ context, params, logger }) => { @@ -225,7 +225,7 @@ const disableSLORoute = createObservabilityServerRoute({ const findSLORoute = createObservabilityServerRoute({ endpoint: 'GET /api/observability/slos 2023-10-31', options: { - tags: ['access:public', 'access:slo_read'], + tags: ['access:slo_read'], }, params: findSLOParamsSchema, handler: async ({ context, params, logger }) => { diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant.ts index 8d8938fc49521..041d2ebcba498 100644 --- a/x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant.ts +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant.ts @@ -7,6 +7,12 @@ import { useContext } from 'react'; import { ObservabilityAIAssistantContext } from '../context/observability_ai_assistant_provider'; +export function useObservabilityAIAssistantOptional() { + const services = useContext(ObservabilityAIAssistantContext); + + return services; +} + export function useObservabilityAIAssistant() { const services = useContext(ObservabilityAIAssistantContext); diff --git a/x-pack/plugins/observability_ai_assistant/public/index.ts b/x-pack/plugins/observability_ai_assistant/public/index.ts index 230106b0d963f..18039652eaa66 100644 --- a/x-pack/plugins/observability_ai_assistant/public/index.ts +++ b/x-pack/plugins/observability_ai_assistant/public/index.ts @@ -32,7 +32,10 @@ export { ObservabilityAIAssistantProvider } from './context/observability_ai_ass export type { ObservabilityAIAssistantPluginSetup, ObservabilityAIAssistantPluginStart }; -export { useObservabilityAIAssistant } from './hooks/use_observability_ai_assistant'; +export { + useObservabilityAIAssistant, + useObservabilityAIAssistantOptional, +} from './hooks/use_observability_ai_assistant'; export type { Conversation, Message } from '../common'; export { MessageRole } from '../common'; diff --git a/x-pack/plugins/rule_registry/common/types.ts b/x-pack/plugins/rule_registry/common/types.ts index 3c0816763e220..da14078b72a3b 100644 --- a/x-pack/plugins/rule_registry/common/types.ts +++ b/x-pack/plugins/rule_registry/common/types.ts @@ -164,6 +164,22 @@ const bucketAggsTempsSchemas: t.Type = t.exact( nested: t.type({ path: t.string, }), + multi_terms: t.exact( + t.partial({ + terms: t.array(t.type({ field: t.string })), + collect_mode: t.string, + min_doc_count: t.number, + shard_min_doc_count: t.number, + size: t.number, + shard_size: t.number, + show_term_doc_count_error: t.boolean, + order: t.union([ + sortOrderSchema, + t.record(t.string, sortOrderSchema), + t.array(t.record(t.string, sortOrderSchema)), + ]), + }) + ), terms: t.exact( t.partial({ field: t.string, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/strip_non_ecs_fields.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/strip_non_ecs_fields.test.ts index 1d9d349dd0b5a..de4e9982c852d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/strip_non_ecs_fields.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/strip_non_ecs_fields.test.ts @@ -551,4 +551,72 @@ describe('stripNonEcsFields', () => { expect(removed).toEqual([]); }); }); + + // geo_point is too complex so we going to skip its validation + describe('geo_point field', () => { + it('should not strip invalid geo_point field', () => { + const { result, removed } = stripNonEcsFields({ + 'client.location.geo': 'invalid geo_point', + }); + + expect(result).toEqual({ + 'client.location.geo': 'invalid geo_point', + }); + expect(removed).toEqual([]); + }); + + it('should not strip valid geo_point fields', () => { + expect( + stripNonEcsFields({ + 'client.geo.location': [0, 90], + }).result + ).toEqual({ + 'client.geo.location': [0, 90], + }); + + expect( + stripNonEcsFields({ + 'client.geo.location': { + type: 'Point', + coordinates: [-88.34, 20.12], + }, + }).result + ).toEqual({ + 'client.geo.location': { + type: 'Point', + coordinates: [-88.34, 20.12], + }, + }); + + expect( + stripNonEcsFields({ + 'client.geo.location': 'POINT (-71.34 41.12)', + }).result + ).toEqual({ + 'client.geo.location': 'POINT (-71.34 41.12)', + }); + + expect( + stripNonEcsFields({ + client: { + geo: { + location: { + lat: 41.12, + lon: -71.34, + }, + }, + }, + }).result + ).toEqual({ + client: { + geo: { + location: { + lat: 41.12, + lon: -71.34, + }, + }, + }, + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/strip_non_ecs_fields.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/strip_non_ecs_fields.ts index 975b2b643a4e7..62e5c9211c1be 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/strip_non_ecs_fields.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/strip_non_ecs_fields.ts @@ -57,10 +57,11 @@ const ecsObjectFields = getEcsObjectFields(); /** * checks if path is a valid Ecs object type (object or flattened) + * geo_point also can be object */ const getIsEcsFieldObject = (path: string) => { const ecsField = ecsFieldMap[path as keyof typeof ecsFieldMap]; - return ['object', 'flattened'].includes(ecsField?.type) || ecsObjectFields[path]; + return ['object', 'flattened', 'geo_point'].includes(ecsField?.type) || ecsObjectFields[path]; }; /** @@ -117,6 +118,11 @@ const computeIsEcsCompliant = (value: SourceField, path: string) => { const ecsField = ecsFieldMap[path as keyof typeof ecsFieldMap]; const isEcsFieldObject = getIsEcsFieldObject(path); + // do not validate geo_point, since it's very complex type that can be string/array/object + if (ecsField?.type === 'geo_point') { + return true; + } + // validate if value is a long type if (ecsField?.type === 'long') { return isValidLongType(value); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/pivot_configuration/pivot_configuration.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/pivot_configuration/pivot_configuration.tsx index 214158606d32e..158d26f8ee9d1 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/pivot_configuration/pivot_configuration.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/pivot_configuration/pivot_configuration.tsx @@ -28,7 +28,8 @@ export const PivotConfiguration: FC = memo( const { ml: { useFieldStatsTrigger, FieldStatsInfoButton }, } = useAppDependencies(); - const { handleFieldStatsButtonClick, closeFlyout, renderOption } = useFieldStatsTrigger(); + const { handleFieldStatsButtonClick, closeFlyout, renderOption, populatedFields } = + useFieldStatsTrigger(); const { addAggregation, @@ -52,6 +53,7 @@ export const PivotConfiguration: FC = memo( // for more robust rendering label: ( = memo( }; return aggOption; }), - [aggOptions, FieldStatsInfoButton, handleFieldStatsButtonClick] + [aggOptions, FieldStatsInfoButton, handleFieldStatsButtonClick, populatedFields] ); return ( diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index f70caee66aec9..dcd9d175820be 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -14873,8 +14873,6 @@ "xpack.enterpriseSearch.workplaceSearch.integrations.azureBlobDescription": "Effectuez des recherches sur votre contenu sur Stockage Blob Azure avec Enterprise Search.", "xpack.enterpriseSearch.workplaceSearch.integrations.boxDescription": "Effectuez des recherches dans vos fichiers et dossiers stockés sur Box avec Workplace Search.", "xpack.enterpriseSearch.workplaceSearch.integrations.boxName": "Box", - "xpack.enterpriseSearch.workplaceSearch.integrations.gmailDescription": "Effectuez des recherches dans vos e-mails gérés par Gmail avec Workplace Search.", - "xpack.enterpriseSearch.workplaceSearch.integrations.gmailName": "Gmail", "xpack.enterpriseSearch.workplaceSearch.integrations.googleCloud": "Google Cloud Storage", "xpack.enterpriseSearch.workplaceSearch.integrations.googleCloudDescription": "Effectuez des recherches sur votre contenu sur Google Cloud Storage avec Enterprise Search.", "xpack.enterpriseSearch.workplaceSearch.integrations.googleDriveDescription": "Effectuez des recherches dans vos documents sur Google Drive avec Workplace Search.", @@ -14900,8 +14898,6 @@ "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointOnlineName": "SharePoint Online", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointServerDescription": "Effectuez des recherches dans vos fichiers stockés sur le serveur Microsoft SharePoint avec Workplace Search.", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointServerName": "Serveur SharePoint", - "xpack.enterpriseSearch.workplaceSearch.integrations.slackDescription": "Effectuez des recherches dans vos messages sur Slack avec Workplace Search.", - "xpack.enterpriseSearch.workplaceSearch.integrations.slackName": "Slack", "xpack.enterpriseSearch.workplaceSearch.integrations.zendeskDescription": "Effectuez des recherches dans vos tickets sur Zendesk avec Workplace Search.", "xpack.enterpriseSearch.workplaceSearch.integrations.zendeskName": "Zendesk", "xpack.enterpriseSearch.workplaceSearch.keepEditing.button": "Continuer la modification", @@ -24808,7 +24804,7 @@ "xpack.ml.newJob.wizard.fieldContextFlyoutCloseButton": "Fermer", "xpack.ml.newJob.wizard.fieldContextFlyoutTitle": "Statistiques de champ", "xpack.ml.newJob.wizard.fieldContextPopover.inspectFieldStatsTooltip": "Inspecter les statistiques de champ", - "xpack.ml.newJob.wizard.fieldContextPopover.inspectFieldStatsTooltipArialabel": "Inspecter les statistiques de champ", + "xpack.ml.newJob.wizard.fieldContextPopover.inspectFieldStatsTooltipAriaLabel": "Inspecter les statistiques de champ", "xpack.ml.newJob.wizard.jobCreatorTitle.advanced": "Avancé", "xpack.ml.newJob.wizard.jobCreatorTitle.categorization": "Catégorisation", "xpack.ml.newJob.wizard.jobCreatorTitle.geo": "Données géographiques", @@ -40464,4 +40460,4 @@ "xpack.painlessLab.walkthroughButtonLabel": "Présentation", "xpack.serverlessObservability.nav.getStarted": "Démarrer" } -} \ No newline at end of file +} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ca71e8c833cb5..f49042924be5d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -14887,8 +14887,6 @@ "xpack.enterpriseSearch.workplaceSearch.integrations.azureBlobDescription": "エンタープライズ サーチでAzure Blob Storageのコンテンツを検索します。", "xpack.enterpriseSearch.workplaceSearch.integrations.boxDescription": "Workplace Searchを使用して、Boxに保存されたファイルとフォルダーを検索します。", "xpack.enterpriseSearch.workplaceSearch.integrations.boxName": "Box", - "xpack.enterpriseSearch.workplaceSearch.integrations.gmailDescription": "Workplace Searchを使用して、Gmailで管理された電子メールを検索します。", - "xpack.enterpriseSearch.workplaceSearch.integrations.gmailName": "Gmail", "xpack.enterpriseSearch.workplaceSearch.integrations.googleCloud": "Google Cloud Storage", "xpack.enterpriseSearch.workplaceSearch.integrations.googleCloudDescription": "エンタープライズ サーチでGoogle Cloud Storageのコンテンツを検索します。", "xpack.enterpriseSearch.workplaceSearch.integrations.googleDriveDescription": "Workplace Searchを使用して、Google Driveのドキュメントを検索します。", @@ -14914,8 +14912,6 @@ "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointOnlineName": "SharePoint Online", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointServerDescription": "Workplace Searchを使用して、Microsoft SharePoint Serverに保存されたファイルを検索します。", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointServerName": "SharePoint Server", - "xpack.enterpriseSearch.workplaceSearch.integrations.slackDescription": "Workplace Searchを使用して、Slackのメッセージを検索します。", - "xpack.enterpriseSearch.workplaceSearch.integrations.slackName": "Slack", "xpack.enterpriseSearch.workplaceSearch.integrations.zendeskDescription": "Workplace Searchを使用して、Zendeskのチケットを検索します。", "xpack.enterpriseSearch.workplaceSearch.integrations.zendeskName": "Zendesk", "xpack.enterpriseSearch.workplaceSearch.keepEditing.button": "編集を続行", @@ -24808,7 +24804,7 @@ "xpack.ml.newJob.wizard.fieldContextFlyoutCloseButton": "閉じる", "xpack.ml.newJob.wizard.fieldContextFlyoutTitle": "フィールド統計情報", "xpack.ml.newJob.wizard.fieldContextPopover.inspectFieldStatsTooltip": "検査フィールド統計情報", - "xpack.ml.newJob.wizard.fieldContextPopover.inspectFieldStatsTooltipArialabel": "検査フィールド統計情報", + "xpack.ml.newJob.wizard.fieldContextPopover.inspectFieldStatsTooltipAriaLabel": "検査フィールド統計情報", "xpack.ml.newJob.wizard.jobCreatorTitle.advanced": "高度な設定", "xpack.ml.newJob.wizard.jobCreatorTitle.categorization": "カテゴリー分け", "xpack.ml.newJob.wizard.jobCreatorTitle.geo": "地理情報", @@ -40455,4 +40451,4 @@ "xpack.painlessLab.walkthroughButtonLabel": "実地検証", "xpack.serverlessObservability.nav.getStarted": "使ってみる" } -} \ No newline at end of file +} diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ef71636689383..1556ea23a7cc3 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -14887,8 +14887,6 @@ "xpack.enterpriseSearch.workplaceSearch.integrations.azureBlobDescription": "使用 Enterprise Search 在 Azure Blob 存储上搜索您的内容。", "xpack.enterpriseSearch.workplaceSearch.integrations.boxDescription": "通过 Workplace Search 搜索存储在 Box 上的文件和文件夹。", "xpack.enterpriseSearch.workplaceSearch.integrations.boxName": "Box", - "xpack.enterpriseSearch.workplaceSearch.integrations.gmailDescription": "通过 Workplace Search 搜索由 Gmail 管理的电子邮件。", - "xpack.enterpriseSearch.workplaceSearch.integrations.gmailName": "Gmail", "xpack.enterpriseSearch.workplaceSearch.integrations.googleCloud": "Google Cloud Storage", "xpack.enterpriseSearch.workplaceSearch.integrations.googleCloudDescription": "使用 Enterprise Search 在 Google Cloud Storage 上搜索您的内容。", "xpack.enterpriseSearch.workplaceSearch.integrations.googleDriveDescription": "通过 Workplace Search 搜索 Google 云端硬盘上的文档。", @@ -14914,8 +14912,6 @@ "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointOnlineName": "Sharepoint", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointServerDescription": "通过 Workplace Search 搜索存储在 Microsoft SharePoint Server 上的文件。", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointServerName": "SharePoint Server", - "xpack.enterpriseSearch.workplaceSearch.integrations.slackDescription": "通过 Workplace Search 搜索 Slack 上的消息。", - "xpack.enterpriseSearch.workplaceSearch.integrations.slackName": "Slack", "xpack.enterpriseSearch.workplaceSearch.integrations.zendeskDescription": "通过 Workplace Search 搜索 Zendesk 上的工单。", "xpack.enterpriseSearch.workplaceSearch.integrations.zendeskName": "Zendesk", "xpack.enterpriseSearch.workplaceSearch.keepEditing.button": "继续编辑", @@ -24807,7 +24803,7 @@ "xpack.ml.newJob.wizard.fieldContextFlyoutCloseButton": "关闭", "xpack.ml.newJob.wizard.fieldContextFlyoutTitle": "字段统计信息", "xpack.ml.newJob.wizard.fieldContextPopover.inspectFieldStatsTooltip": "检查字段统计信息", - "xpack.ml.newJob.wizard.fieldContextPopover.inspectFieldStatsTooltipArialabel": "检查字段统计信息", + "xpack.ml.newJob.wizard.fieldContextPopover.inspectFieldStatsTooltipAriaLabel": "检查字段统计信息", "xpack.ml.newJob.wizard.jobCreatorTitle.advanced": "高级", "xpack.ml.newJob.wizard.jobCreatorTitle.categorization": "归类", "xpack.ml.newJob.wizard.jobCreatorTitle.geo": "地理", @@ -40449,4 +40445,4 @@ "xpack.painlessLab.walkthroughButtonLabel": "指导", "xpack.serverlessObservability.nav.getStarted": "开始使用" } -} \ No newline at end of file +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/install_resources.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/install_resources.ts index e80a8f94d93b6..b6c86b49c7fba 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/install_resources.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/install_resources.ts @@ -163,7 +163,6 @@ export default function createAlertsAsDataInstallResourcesTest({ getService }: F rollover_alias: '.alerts-test.patternfiring.alerts-default', }, mapping: { - ignore_malformed: 'true', total_fields: { limit: '2500', }, @@ -197,7 +196,6 @@ export default function createAlertsAsDataInstallResourcesTest({ getService }: F }); expect(contextIndex[indexName].settings?.index?.mapping).to.eql({ - ignore_malformed: 'true', total_fields: { limit: '2500', }, diff --git a/x-pack/test/api_integration/apis/metrics_ui/log_entry_highlights.ts b/x-pack/test/api_integration/apis/metrics_ui/log_entry_highlights.ts index f1be3edf0f4ac..1e565f63b0733 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/log_entry_highlights.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/log_entry_highlights.ts @@ -45,8 +45,7 @@ export default function ({ getService }: FtrProviderContext) { after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/simple_logs')); describe('/log_entries/highlights', () => { - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/163486 - describe.skip('with the default source', () => { + describe('with the default source', () => { before(() => kibanaServer.savedObjects.cleanStandardList()); after(() => kibanaServer.savedObjects.cleanStandardList()); @@ -120,7 +119,7 @@ export default function ({ getService }: FtrProviderContext) { entries.forEach((entry) => { entry.columns.forEach((column) => { if ('message' in column && 'highlights' in column.message[0]) { - expect(column.message[0].highlights).to.eql(['message', 'of', 'document', '0']); + expect(column.message[0].highlights).to.eql(['message of document 0']); } }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/machine_learning.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/machine_learning.ts index be0d4876b59af..9450c32210009 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/machine_learning.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/machine_learning.ts @@ -66,8 +66,7 @@ export default ({ getService }: FtrProviderContext) => { rule_id: 'ml-rule-id', }; - // FLAKY: https://github.com/elastic/kibana/issues/145776 - describe.skip('Machine learning type rules', () => { + describe('Machine learning type rules', () => { before(async () => { // Order is critical here: auditbeat data must be loaded before attempting to start the ML job, // as the job looks for certain indices on start diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/non_ecs_fields.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/non_ecs_fields.ts index f315dfabb4d86..32ae758b20807 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/non_ecs_fields.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/non_ecs_fields.ts @@ -56,6 +56,7 @@ export default ({ getService }: FtrProviderContext) => { }; }; + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/154277 describe('Non ECS fields in alert document source', () => { before(async () => { await esArchiver.load( @@ -258,7 +259,6 @@ export default ({ getService }: FtrProviderContext) => { // we don't validate it because geo_point is very complex type with many various representations: array, different object, string with few valid patterns // more on geo_point type https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-point.html - // since .alerts-* indices allow _ignore_malformed option, alert will be indexed for this document it('should fail creating alert when ECS field mapping is geo_point', async () => { const document = { client: { @@ -269,11 +269,12 @@ export default ({ getService }: FtrProviderContext) => { }, }; - const { errors, alertSource } = await indexAndCreatePreviewAlert(document); - - expect(errors).toEqual([]); + const { errors } = await indexAndCreatePreviewAlert(document); - expect(alertSource).toHaveProperty('client.geo.location', 'test test'); + expect(errors[0]).toContain('Bulk Indexing of signals failed'); + expect(errors[0]).toContain( + 'failed to parse field [client.geo.location] of type [geo_point]' + ); }); it('should strip invalid boolean values and left valid ones', async () => { diff --git a/x-pack/test/detection_engine_api_integration/utils/machine_learning_setup.ts b/x-pack/test/detection_engine_api_integration/utils/machine_learning_setup.ts index 84eeeea137cb0..47b870496642b 100644 --- a/x-pack/test/detection_engine_api_integration/utils/machine_learning_setup.ts +++ b/x-pack/test/detection_engine_api_integration/utils/machine_learning_setup.ts @@ -6,6 +6,7 @@ */ import type SuperTest from 'supertest'; +import { getCommonRequestHeader } from '../../functional/services/ml/common_api'; export const executeSetupModuleRequest = async ({ module, @@ -18,7 +19,7 @@ export const executeSetupModuleRequest = async ({ }) => { const { body } = await supertest .post(`/internal/ml/modules/setup/${module}`) - .set('kbn-xsrf', 'true') + .set(getCommonRequestHeader('1')) .send({ prefix: '', groups: ['auditbeat'], @@ -42,8 +43,8 @@ export const forceStartDatafeeds = async ({ supertest: SuperTest.SuperTest; }) => { const { body } = await supertest - .post(`/supertest/ml/jobs/force_start_datafeeds`) - .set('kbn-xsrf', 'true') + .post(`/internal/ml/jobs/force_start_datafeeds`) + .set(getCommonRequestHeader('1')) .send({ datafeedIds: [`datafeed-${jobId}`], start: new Date().getUTCMilliseconds(), diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts index 3744c2aa9d2f0..aaf31e54798db 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts @@ -437,61 +437,75 @@ const expectAssetsInstalled = ({ id: 'sample_dashboard', }); expect(resDashboard.id).equal('sample_dashboard'); + expect(resDashboard.managed).be(true); expect(resDashboard.references.map((ref: any) => ref.id).includes('sample_tag')).equal(true); const resDashboard2 = await kibanaServer.savedObjects.get({ type: 'dashboard', id: 'sample_dashboard2', }); expect(resDashboard2.id).equal('sample_dashboard2'); + expect(resDashboard2.managed).be(true); const resVis = await kibanaServer.savedObjects.get({ type: 'visualization', id: 'sample_visualization', }); + expect(resVis.id).equal('sample_visualization'); + expect(resVis.managed).be(true); const resSearch = await kibanaServer.savedObjects.get({ type: 'search', id: 'sample_search', }); expect(resSearch.id).equal('sample_search'); + expect(resSearch.managed).be(true); const resLens = await kibanaServer.savedObjects.get({ type: 'lens', id: 'sample_lens', }); + expect(resLens.id).equal('sample_lens'); + expect(resLens.managed).be(true); const resMlModule = await kibanaServer.savedObjects.get({ type: 'ml-module', id: 'sample_ml_module', }); expect(resMlModule.id).equal('sample_ml_module'); + expect(resMlModule.managed).be(true); const resSecurityRule = await kibanaServer.savedObjects.get({ type: 'security-rule', id: 'sample_security_rule', }); expect(resSecurityRule.id).equal('sample_security_rule'); + expect(resSecurityRule.managed).be(true); const resOsqueryPackAsset = await kibanaServer.savedObjects.get({ type: 'osquery-pack-asset', id: 'sample_osquery_pack_asset', }); expect(resOsqueryPackAsset.id).equal('sample_osquery_pack_asset'); + expect(resOsqueryPackAsset.managed).be(true); const resOsquerySavedObject = await kibanaServer.savedObjects.get({ type: 'osquery-saved-query', id: 'sample_osquery_saved_query', }); expect(resOsquerySavedObject.id).equal('sample_osquery_saved_query'); + expect(resOsquerySavedObject.managed).be(true); const resCloudSecurityPostureRuleTemplate = await kibanaServer.savedObjects.get({ type: 'csp-rule-template', id: 'sample_csp_rule_template', }); expect(resCloudSecurityPostureRuleTemplate.id).equal('sample_csp_rule_template'); + expect(resCloudSecurityPostureRuleTemplate.managed).be(true); const resTag = await kibanaServer.savedObjects.get({ type: 'tag', id: 'sample_tag', }); + expect(resTag.managed).be(true); expect(resTag.id).equal('sample_tag'); const resIndexPattern = await kibanaServer.savedObjects.get({ type: 'index-pattern', id: 'test-*', }); + expect(resIndexPattern.managed).be(true); expect(resIndexPattern.id).equal('test-*'); let resInvalidTypeIndexPattern; diff --git a/x-pack/test/fleet_api_integration/apis/policy_secrets.ts b/x-pack/test/fleet_api_integration/apis/policy_secrets.ts index 34f20e88b0a81..52b614f389ba9 100644 --- a/x-pack/test/fleet_api_integration/apis/policy_secrets.ts +++ b/x-pack/test/fleet_api_integration/apis/policy_secrets.ts @@ -41,37 +41,43 @@ function createdPolicyToUpdatePolicy(policy: any) { return updatedPolicy; } +const SECRETS_INDEX_NAME = '.fleet-secrets'; export default function (providerContext: FtrProviderContext) { - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/162732 - describe.skip('fleet policy secrets', () => { + describe('fleet policy secrets', () => { const { getService } = providerContext; const es: Client = getService('es'); const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); - const getPackagePolicyById = async (id: string) => { - const { body } = await supertest.get(`/api/fleet/package_policies/${id}`); - return body.item; + const getSecrets = async (ids?: string[]) => { + const query = ids ? { terms: { _id: ids } } : { match_all: {} }; + return es.search({ + index: SECRETS_INDEX_NAME, + body: { + query, + }, + }); }; - const maybeCreateSecretsIndex = async () => { - // create mock .secrets index for testing - if (await es.indices.exists({ index: '.fleet-test-secrets' })) { - await es.indices.delete({ index: '.fleet-test-secrets' }); - } - await es.indices.create({ - index: '.fleet-test-secrets', - body: { - mappings: { - properties: { - value: { - type: 'keyword', - }, + const deleteAllSecrets = async () => { + try { + await es.deleteByQuery({ + index: SECRETS_INDEX_NAME, + body: { + query: { + match_all: {}, }, }, - }, - }); + }); + } catch (err) { + // index doesnt exis + } + }; + + const getPackagePolicyById = async (id: string) => { + const { body } = await supertest.get(`/api/fleet/package_policies/${id}`); + return body.item; }; const getFullAgentPolicyById = async (id: string) => { @@ -137,10 +143,8 @@ export default function (providerContext: FtrProviderContext) { let agentPolicyId: string; before(async () => { await kibanaServer.savedObjects.cleanStandardList(); - await getService('esArchiver').load( - 'x-pack/test/functional/es_archives/fleet/empty_fleet_server' - ); - await maybeCreateSecretsIndex(); + + await deleteAllSecrets(); }); setupFleetAndAgents(providerContext); @@ -261,16 +265,7 @@ export default function (providerContext: FtrProviderContext) { }); it('should have correctly created the secrets', async () => { - const searchRes = await es.search({ - index: '.fleet-test-secrets', - body: { - query: { - ids: { - values: [packageVarId, inputVarId, streamVarId], - }, - }, - }, - }); + const searchRes = await getSecrets([packageVarId, inputVarId, streamVarId]); expect(searchRes.hits.hits.length).to.eql(3); @@ -337,14 +332,7 @@ export default function (providerContext: FtrProviderContext) { }); it('should have correctly deleted unused secrets after update', async () => { - const searchRes = await es.search({ - index: '.fleet-test-secrets', - body: { - query: { - match_all: {}, - }, - }, - }); + const searchRes = await getSecrets(); expect(searchRes.hits.hits.length).to.eql(3); // should have created 1 and deleted 1 doc @@ -374,14 +362,7 @@ export default function (providerContext: FtrProviderContext) { expectCompiledPolicyVars(policyDoc, updatedPackageVarId); - const searchRes = await es.search({ - index: '.fleet-test-secrets', - body: { - query: { - match_all: {}, - }, - }, - }); + const searchRes = await getSecrets(); expect(searchRes.hits.hits.length).to.eql(3); @@ -413,53 +394,36 @@ export default function (providerContext: FtrProviderContext) { updatedPackagePolicy.vars.package_var_secret.value.id, updatedPackageVarId, ]; - - const searchRes = await es.search({ - index: '.fleet-test-secrets', - body: { - query: { - terms: { - _id: packageVarSecretIds, - }, - }, - }, - }); + const searchRes = await getSecrets(packageVarSecretIds); expect(searchRes.hits.hits.length).to.eql(2); }); it('should not delete used secrets on package policy delete', async () => { - return supertest + await supertest .delete(`/api/fleet/package_policies/${duplicatedPackagePolicyId}`) .set('kbn-xsrf', 'xxxx') .expect(200); - const searchRes = await es.search({ - index: '.fleet-test-secrets', - body: { - query: { - match_all: {}, - }, - }, - }); + // sleep to allow for secrets to be deleted + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const searchRes = await getSecrets(); + // should have deleted new_package_secret_val_2 expect(searchRes.hits.hits.length).to.eql(3); }); it('should delete all secrets on package policy delete', async () => { - return supertest + await supertest .delete(`/api/fleet/package_policies/${createdPackagePolicyId}`) .set('kbn-xsrf', 'xxxx') .expect(200); - const searchRes = await es.search({ - index: '.fleet-test-secrets', - body: { - query: { - match_all: {}, - }, - }, - }); + // sleep to allow for secrets to be deleted + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const searchRes = await getSecrets(); expect(searchRes.hits.hits.length).to.eql(0); }); diff --git a/x-pack/test/fleet_api_integration/config.base.ts b/x-pack/test/fleet_api_integration/config.base.ts index e5746278a26f9..3e4b35988efba 100644 --- a/x-pack/test/fleet_api_integration/config.base.ts +++ b/x-pack/test/fleet_api_integration/config.base.ts @@ -74,7 +74,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { 'secretsStorage', 'agentTamperProtectionEnabled', ])}`, - `--xpack.fleet.developer.testSecretsIndex=.fleet-test-secrets`, `--logging.loggers=${JSON.stringify([ ...getKibanaCliLoggers(xPackAPITestsConfig.get('kbnTestServer.serverArgs')), diff --git a/x-pack/test/functional/apps/discover/async_scripted_fields.js b/x-pack/test/functional/apps/discover/async_scripted_fields.js index 9a9d5e0d450f2..0d48f42c5ba1e 100644 --- a/x-pack/test/functional/apps/discover/async_scripted_fields.js +++ b/x-pack/test/functional/apps/discover/async_scripted_fields.js @@ -15,9 +15,17 @@ export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const log = getService('log'); const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common', 'settings', 'discover', 'timePicker']); + const PageObjects = getPageObjects([ + 'common', + 'settings', + 'discover', + 'timePicker', + 'header', + 'dashboard', + ]); const queryBar = getService('queryBar'); const security = getService('security'); + const dashboardAddPanel = getService('dashboardAddPanel'); describe('async search with scripted fields', function () { this.tags(['skipFirefox']); @@ -43,7 +51,7 @@ export default function ({ getService, getPageObjects }) { await security.testUser.restoreDefaults(); }); - it('query should show failed shards pop up', async function () { + it('query should show failed shards callout', async function () { if (false) { /* If you had to modify the scripted fields, you could un-comment all this, run it, use es_archiver to update 'kibana_scripted_fields_on_logstash' */ @@ -69,12 +77,39 @@ export default function ({ getService, getPageObjects }) { await retry.tryForTime(20000, async function () { // wait for shards failed message - const shardMessage = await testSubjects.getVisibleText('euiToastHeader'); + const shardMessage = await testSubjects.getVisibleText( + 'dscNoResultsInterceptedWarningsCallout_warningTitle' + ); log.debug(shardMessage); expect(shardMessage).to.be('1 of 3 shards failed'); }); }); + it('query should show failed shards badge on dashboard', async function () { + await security.testUser.setRoles([ + 'test_logstash_reader', + 'global_discover_all', + 'global_dashboard_all', + ]); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.selectIndexPattern('logsta*'); + + await PageObjects.discover.saveSearch('search with warning'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.clickNewDashboard(); + + await dashboardAddPanel.addSavedSearch('search with warning'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await retry.tryForTime(20000, async function () { + // wait for shards failed message + await testSubjects.existOrFail('savedSearchEmbeddableWarningsCallout_trigger'); + }); + }); + it('query return results with valid scripted field', async function () { if (false) { /* the skipped steps below were used to create the scripted fields in the logstash-* index pattern diff --git a/x-pack/test/functional/page_objects/infra_hosts_view.ts b/x-pack/test/functional/page_objects/infra_hosts_view.ts index 6bad2bf1630e5..25e4b0302a763 100644 --- a/x-pack/test/functional/page_objects/infra_hosts_view.ts +++ b/x-pack/test/functional/page_objects/infra_hosts_view.ts @@ -12,14 +12,6 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); return { - async clickTryHostViewLink() { - return await testSubjects.click('inventory-hostsView-link'); - }, - - async clickTryHostViewBadge() { - return await testSubjects.click('inventory-hostsView-link-badge'); - }, - async clickTableOpenFlyoutButton() { return testSubjects.click('hostsView-flyout-button'); }, @@ -40,32 +32,47 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { return testSubjects.click('euiFlyoutCloseButton'); }, + async getBetaBadgeExists() { + return testSubjects.exists('infra-beta-badge'); + }, + + // Inventory UI + async clickTryHostViewLink() { + return await testSubjects.click('inventory-hostsView-link'); + }, + + async clickTryHostViewBadge() { + return await testSubjects.click('inventory-hostsView-link-badge'); + }, + + // Asset Details Flyout + async clickOverviewFlyoutTab() { - return testSubjects.click('hostsView-flyout-tabs-overview'); + return testSubjects.click('infraAssetDetailsOverviewTab'); }, async clickMetadataFlyoutTab() { - return testSubjects.click('hostsView-flyout-tabs-metadata'); + return testSubjects.click('infraAssetDetailsMetadataTab'); }, - async clickOverviewLinkToAlerts() { - return testSubjects.click('assetDetails-flyout-alerts-link'); + async clickProcessesFlyoutTab() { + return testSubjects.click('infraAssetDetailsProcessesTab'); }, - async clickOverviewOpenAlertsFlyout() { - return testSubjects.click('infraNodeContextPopoverCreateInventoryRuleButton'); + async clickLogsFlyoutTab() { + return testSubjects.click('infraAssetDetailsLogsTab'); }, - async clickProcessesFlyoutTab() { - return testSubjects.click('hostsView-flyout-tabs-processes'); + async clickOverviewLinkToAlerts() { + return testSubjects.click('infraAssetDetailsAlertsShowAllButton'); }, - async clickShowAllMetadataOverviewTab() { - return testSubjects.click('infraMetadataSummaryShowAllMetadataButton'); + async clickOverviewOpenAlertsFlyout() { + return testSubjects.click('infraAssetDetailsCreateAlertsRuleButton'); }, - async clickLogsFlyoutTab() { - return testSubjects.click('hostsView-flyout-tabs-logs'); + async clickShowAllMetadataOverviewTab() { + return testSubjects.click('infraAssetDetailsMetadataShowAllButton'); }, async clickProcessesTableExpandButton() { @@ -73,28 +80,26 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { }, async clickFlyoutApmServicesLink() { - return testSubjects.click('hostsView-flyout-apm-services-link'); + return testSubjects.click('infraAssetDetailsViewAPMServicesButton'); }, async clickAddMetadataPin() { - return testSubjects.click('infraMetadataEmbeddableAddPin'); + return testSubjects.click('infraAssetDetailsMetadataAddPin'); }, async clickRemoveMetadataPin() { - return testSubjects.click('infraMetadataEmbeddableRemovePin'); + return testSubjects.click('infraAssetDetailsMetadataRemovePin'); }, async clickAddMetadataFilter() { - return testSubjects.click('hostsView-flyout-metadata-add-filter'); + return testSubjects.click('infraAssetDetailsMetadataAddFilterButton'); }, async clickRemoveMetadataFilter() { - return testSubjects.click('hostsView-flyout-metadata-remove-filter'); + return testSubjects.click('infraAssetDetailsMetadataRemoveFilterButton'); }, - async getBetaBadgeExists() { - return testSubjects.exists('infra-beta-badge'); - }, + // Splash screen async getHostsLandingPageDisabled() { const container = await testSubjects.find('hostView-no-enable-access'); @@ -203,39 +208,29 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { return div.getAttribute('title'); }, - // Flyout Tabs + // Asset Details Flyout Tabs async getAssetDetailsKPITileValue(type: string) { - const container = await testSubjects.find('assetDetailsKPIGrid'); + const container = await testSubjects.find('infraAssetDetailsKPIGrid'); const element = await container.findByTestSubject(`infraAssetDetailsKPI${type}`); const div = await element.findByClassName('echMetricText__value'); return div.getAttribute('title'); }, overviewAlertsTitleExist() { - return testSubjects.exists('assetDetailsAlertsTitle'); + return testSubjects.exists('infraAssetDetailsAlertsTitle'); }, async getAssetDetailsMetricsCharts() { - const container = await testSubjects.find('assetDetailsMetricsChartGrid'); + const container = await testSubjects.find('infraAssetDetailsMetricsChartGrid'); return container.findAllByCssSelector('[data-test-subj*="infraAssetDetailsMetricsChart"]'); }, - getMetadataTab() { - return testSubjects.find('hostsView-flyout-tabs-metadata'); - }, - metadataTableExist() { - return testSubjects.exists('infraMetadataTable'); - }, - - async getMetadataTabName() { - const tabElement = await this.getMetadataTab(); - const tabTitle = await tabElement.findByClassName('euiTab__content'); - return tabTitle.getVisibleText(); + return testSubjects.exists('infraAssetDetailsMetadataTable'); }, async getRemovePinExist() { - return testSubjects.exists('infraMetadataEmbeddableRemovePin'); + return testSubjects.exists('infraAssetDetailsMetadataRemovePin'); }, async getAppliedFilter() { @@ -246,21 +241,25 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { }, async getRemoveFilterExist() { - return testSubjects.exists('hostsView-flyout-metadata-remove-filter'); + return testSubjects.exists('infraAssetDetailsMetadataRemoveFilterButton'); }, async getProcessesTabContentTitle(index: number) { - const processesListElements = await testSubjects.findAll('infraProcessesSummaryTableItem'); + const processesListElements = await testSubjects.findAll( + 'infraAssetDetailsProcessesSummaryTableItem' + ); return processesListElements[index].findByCssSelector('dt'); }, async getProcessesTabContentTotalValue() { - const processesListElements = await testSubjects.findAll('infraProcessesSummaryTableItem'); + const processesListElements = await testSubjects.findAll( + 'infraAssetDetailsProcessesSummaryTableItem' + ); return processesListElements[0].findByCssSelector('dd'); }, getProcessesTable() { - return testSubjects.find('infraProcessesTable'); + return testSubjects.find('infraAssetDetailsProcessesTable'); }, async getProcessesTableBody() { diff --git a/x-pack/test/functional/services/observability/alerts/add_to_case.ts b/x-pack/test/functional/services/observability/alerts/add_to_case.ts index 7211325fad2a1..6197273243432 100644 --- a/x-pack/test/functional/services/observability/alerts/add_to_case.ts +++ b/x-pack/test/functional/services/observability/alerts/add_to_case.ts @@ -52,7 +52,8 @@ export function ObservabilityAlertsAddToCaseProvider({ getService }: FtrProvider }; const closeFlyout = async () => { - return await (await testSubjects.find('euiFlyoutCloseButton')).click(); + await testSubjects.click('euiFlyoutCloseButton'); // click close button + await testSubjects.missingOrFail('euiFlyoutCloseButton'); // wait for flyout to be closed }; const getAddToExistingCaseModalOrFail = async () => { diff --git a/x-pack/test/functional/services/observability/alerts/common.ts b/x-pack/test/functional/services/observability/alerts/common.ts index 7a3f1f609a403..5986de63a2d74 100644 --- a/x-pack/test/functional/services/observability/alerts/common.ts +++ b/x-pack/test/functional/services/observability/alerts/common.ts @@ -20,6 +20,7 @@ const DATE_WITH_DATA = { const ALERTS_FLYOUT_SELECTOR = 'alertsFlyout'; const FILTER_FOR_VALUE_BUTTON_SELECTOR = 'filterForValue'; const ALERTS_TABLE_CONTAINER_SELECTOR = 'alertsTable'; +const ALERTS_TABLE_ACTIONS_MENU_SELECTOR = 'alertsTableActionsMenu'; const VIEW_RULE_DETAILS_SELECTOR = 'viewRuleDetails'; const VIEW_RULE_DETAILS_FLYOUT_SELECTOR = 'viewRuleDetailsFlyout'; @@ -209,6 +210,7 @@ export function ObservabilityAlertsCommonProvider({ const openActionsMenuForRow = retryOnStale.wrap(async (rowIndex: number) => { const actionsOverflowButton = await getActionsButtonByIndex(rowIndex); await actionsOverflowButton.click(); + await testSubjects.existOrFail(ALERTS_TABLE_ACTIONS_MENU_SELECTOR); }); const viewRuleDetailsButtonClick = async () => { diff --git a/x-pack/test/functional/services/observability/users.ts b/x-pack/test/functional/services/observability/users.ts index 4e33afe270223..ba67ce8602f50 100644 --- a/x-pack/test/functional/services/observability/users.ts +++ b/x-pack/test/functional/services/observability/users.ts @@ -11,9 +11,11 @@ import { FtrProviderContext } from '../../ftr_provider_context'; type CreateRolePayload = Pick; const OBSERVABILITY_TEST_ROLE_NAME = 'observability-functional-test-role'; +const HOME_PAGE_SELECTOR = 'homeApp'; export function ObservabilityUsersProvider({ getPageObject, getService }: FtrProviderContext) { const security = getService('security'); + const testSubjects = getService('testSubjects'); const commonPageObject = getPageObject('common'); /** @@ -24,7 +26,8 @@ export function ObservabilityUsersProvider({ getPageObject, getService }: FtrPro */ const setTestUserRole = async (roleDefinition: CreateRolePayload) => { // return to neutral grounds to avoid running into permission problems on reload - await commonPageObject.navigateToActualUrl('kibana'); + await commonPageObject.navigateToActualUrl('home'); + await testSubjects.existOrFail(HOME_PAGE_SELECTOR); await security.role.create(OBSERVABILITY_TEST_ROLE_NAME, roleDefinition); diff --git a/x-pack/test/licensing_plugin/scenario.ts b/x-pack/test/licensing_plugin/scenario.ts index f7d2f5790460d..051a937d98d35 100644 --- a/x-pack/test/licensing_plugin/scenario.ts +++ b/x-pack/test/licensing_plugin/scenario.ts @@ -74,18 +74,18 @@ export function createScenario({ getService, getPageObjects }: FtrProviderContex .post('/_license/?acknowledge=true') .send({ license: { - uid: '00000000-d3ad-7357-c0d3-000000000000', + uid: '504430e6-503c-4316-85cb-b402c730ca08', type: 'enterprise', - issue_date_in_millis: 1577836800000, - start_date_in_millis: 1577836800000, - // expires 2022-12-31 - expiry_date_in_millis: 1672531199999, + issue_date_in_millis: 1669680000000, + start_date_in_millis: 1669680000000, + // expires 2024-12-31 + expiry_date_in_millis: 1735689599999, max_resource_units: 250, max_nodes: null, - issued_to: 'Elastic Internal Use (development environments)', - issuer: 'Elastic', + issued_to: 'Elastic - INTERNAL (development environments)', + issuer: 'API', signature: - 'AAAABQAAAA1gHUVis7hel8b8nNCAAAAAIAo5/x6hrsGh1GqqrJmy4qgmEC7gK0U4zQ6q5ZEMhm4jAAABAKMR+w3KZsMJfG5jNWgZXJLwRmiNqN7k94vKFgRdj1yM+gA9ufhXIn9d01OvFhPjilIqm+fxVjCxXwGKbFRiwtTWnTYjXPuNml+qCFGgUWguWEcVoIW6VU7/lYOqMJ4EB4zOMLe93P267iaDm542aelQrW1OJ69lGGuPBik8v9r1bNZzKBQ99VUr/qoosGDAm0udh2HxWzYoCL5lDML5Niy87xlVCubSSBXdUXzUgdZKKk6pKaMdHswB1gjvEfnwqPxEWAyrV0BCr/T1WehXd7U4p6/zt6sJ6cPh+34AZe9g4+3WPKrZhX4iaSHMDDHn4HNjO72CZ2oi42ZDNnJ37tA=', + 'AAAABQAAAA2h1vBafHuRhjOHREKYAAAAIAo5/x6hrsGh1GqqrJmy4qgmEC7gK0U4zQ6q5ZEMhm4jAAABAByGz9MmRW/L7vQriISa6u8Oov7zykA+Cv55BToWEthSn0c5KQUxcWG+K5Cm4/OkFsXA8TE4zFnlSgYxmQi2Eqq7IAKGdcxI/xhQfMsq5RWlSEwtfyV0M2RKJxgam8o2lvKC9EbrU76ISYr7jTkgoBl6GFSjdfXMHmxNXBSKDDm03ZeXkWkvuNNFrHJuYivf2Se9OeeB/eu4jqUI0UuNfPYF07ZcYvtKfj3KX+aysCSV2FW8wgyAjndOPEinfYcwAJ09zcl+MTig2K0DQTsYkLykXmzZnLz6qeuVVFjCTowxizDFW+5MrpzUnwkjqv8CFhLfvxG7waWQWslv8fXLUn8=', }, }) .auth('license_manager_user', 'license_manager_user-password') diff --git a/x-pack/test/licensing_plugin/server/updates.ts b/x-pack/test/licensing_plugin/server/updates.ts index 6acbe42bc1abe..ccec87dc0cdc6 100644 --- a/x-pack/test/licensing_plugin/server/updates.ts +++ b/x-pack/test/licensing_plugin/server/updates.ts @@ -17,8 +17,7 @@ export default function (ftrContext: FtrProviderContext) { const scenario = createScenario(ftrContext); - // FLAKY: https://github.com/elastic/kibana/issues/110938 - describe.skip('changes in license types', () => { + describe('changes in license types', () => { after(async () => { await scenario.teardown(); }); diff --git a/x-pack/test/observability_functional/apps/observability/pages/alerts/add_to_case.ts b/x-pack/test/observability_functional/apps/observability/pages/alerts/add_to_case.ts index cd33c64cc0183..062678ec989ca 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/alerts/add_to_case.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/alerts/add_to_case.ts @@ -25,8 +25,7 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts'); }); - // FLAKY: https://github.com/elastic/kibana/issues/156312 - describe.skip('When user has all privileges for cases', () => { + describe('When user has all privileges for cases', () => { before(async () => { await observability.users.setTestUserRole( observability.users.defineBasicObservabilityRole({ @@ -42,9 +41,7 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { }); it('renders case options in the overflow menu', async () => { - await retry.try(async () => { - await observability.alerts.common.openActionsMenuForRow(0); - }); + await observability.alerts.common.openActionsMenuForRow(0); await retry.try(async () => { await observability.alerts.addToCase.getAddToExistingCaseSelectorOrFail(); @@ -64,9 +61,7 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { }); it('opens a modal when Add to existing case is clicked', async () => { - await retry.try(async () => { - await observability.alerts.common.openActionsMenuForRow(0); - }); + await observability.alerts.common.openActionsMenuForRow(0); await retry.try(async () => { await observability.alerts.addToCase.addToExistingCaseButtonClick(); @@ -91,9 +86,8 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { }); it('does not render case options in the overflow menu', async () => { - await retry.try(async () => { - await observability.alerts.common.openActionsMenuForRow(0); - }); + await observability.alerts.common.openActionsMenuForRow(0); + await retry.try(async () => { await observability.alerts.addToCase.missingAddToExistingCaseSelectorOrFail(); await observability.alerts.addToCase.missingAddToNewCaseSelectorOrFail(); diff --git a/yarn.lock b/yarn.lock index 39981aa19923e..a7e1003917f3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5220,6 +5220,10 @@ version "0.0.0" uid "" +"@kbn/search-response-warnings@link:packages/kbn-search-response-warnings": + version "0.0.0" + uid "" + "@kbn/searchprofiler-plugin@link:x-pack/plugins/searchprofiler": version "0.0.0" uid ""