diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index ac929afea575bd..176e15788e7ceb 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -415,6 +415,7 @@ enabled: - x-pack/test_serverless/api_integration/test_suites/security/common_configs/config.group1.ts - x-pack/test_serverless/functional/test_suites/observability/config.ts - x-pack/test_serverless/functional/test_suites/observability/config.examples.ts + - x-pack/test_serverless/functional/test_suites/observability/config.saved_objects_management.ts - x-pack/test_serverless/functional/test_suites/observability/common_configs/config.group1.ts - x-pack/test_serverless/functional/test_suites/observability/common_configs/config.group2.ts - x-pack/test_serverless/functional/test_suites/observability/common_configs/config.group3.ts @@ -426,6 +427,7 @@ enabled: - x-pack/test_serverless/functional/test_suites/search/config.ts - x-pack/test_serverless/functional/test_suites/search/config.examples.ts - x-pack/test_serverless/functional/test_suites/search/config.screenshots.ts + - x-pack/test_serverless/functional/test_suites/search/config.saved_objects_management.ts - x-pack/test_serverless/functional/test_suites/search/common_configs/config.group1.ts - x-pack/test_serverless/functional/test_suites/search/common_configs/config.group2.ts - x-pack/test_serverless/functional/test_suites/search/common_configs/config.group3.ts @@ -435,6 +437,7 @@ enabled: - x-pack/test_serverless/functional/test_suites/security/config.ts - x-pack/test_serverless/functional/test_suites/security/config.examples.ts - x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.ts + - x-pack/test_serverless/functional/test_suites/security/config.saved_objects_management.ts - x-pack/test_serverless/functional/test_suites/security/common_configs/config.group1.ts - x-pack/test_serverless/functional/test_suites/security/common_configs/config.group2.ts - x-pack/test_serverless/functional/test_suites/security/common_configs/config.group3.ts @@ -516,4 +519,4 @@ enabled: - x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_read/configs/serverless.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_read/configs/ess.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_bulk_actions/configs/ess.config.ts - - x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_bulk_actions/configs/serverless.config.ts \ No newline at end of file + - x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_bulk_actions/configs/serverless.config.ts diff --git a/.buildkite/pipelines/performance/daily.yml b/.buildkite/pipelines/performance/daily.yml index b1a53babb03ac3..8a28ed2a561c3e 100644 --- a/.buildkite/pipelines/performance/daily.yml +++ b/.buildkite/pipelines/performance/daily.yml @@ -21,11 +21,11 @@ steps: queue: kb-static-ubuntu depends_on: build key: tests - timeout_in_minutes: 60 + timeout_in_minutes: 90 retry: automatic: - exit_status: '-1' - limit: 3 + limit: 2 - exit_status: '*' limit: 1 diff --git a/.buildkite/scripts/common/util.sh b/.buildkite/scripts/common/util.sh index 200adce515c062..f80c89678c2210 100755 --- a/.buildkite/scripts/common/util.sh +++ b/.buildkite/scripts/common/util.sh @@ -171,15 +171,23 @@ download_artifact() { retry 3 1 timeout 3m buildkite-agent artifact download "$@" } +# TODO: remove after https://github.com/elastic/kibana-operations/issues/15 is done +if [[ "${VAULT_ADDR:-}" == *"secrets.elastic.co"* ]]; then + VAULT_PATH_PREFIX="secret/kibana-issues/dev" + VAULT_KV_PREFIX="secret/kibana-issues/dev" + IS_LEGACY_VAULT_ADDR=true +else + VAULT_PATH_PREFIX="secret/ci/elastic-kibana" + VAULT_KV_PREFIX="kv/ci-shared/kibana-deployments" + IS_LEGACY_VAULT_ADDR=false +fi +export IS_LEGACY_VAULT_ADDR vault_get() { key_path=$1 field=$2 - fullPath="secret/ci/elastic-kibana/$key_path" - if [[ "$VAULT_ADDR" == *"secrets.elastic.co"* ]]; then - fullPath="secret/kibana-issues/dev/$key_path" - fi + fullPath="$VAULT_PATH_PREFIX/$key_path" if [[ -z "${2:-}" || "${2:-}" =~ ^-.* ]]; then retry 5 5 vault read "$fullPath" "${@:2}" @@ -193,11 +201,17 @@ vault_set() { shift fields=("$@") - fullPath="secret/ci/elastic-kibana/$key_path" - if [[ "$VAULT_ADDR" == *"secrets.elastic.co"* ]]; then - fullPath="secret/kibana-issues/dev/$key_path" - fi + + fullPath="$VAULT_PATH_PREFIX/$key_path" # shellcheck disable=SC2068 retry 5 5 vault write "$fullPath" ${fields[@]} } + +vault_kv_set() { + kv_path=$1 + shift + fields=("$@") + + vault kv put "$VAULT_KV_PREFIX/$kv_path" "${fields[@]}" +} diff --git a/.buildkite/scripts/pipelines/pull_request/pipeline.ts b/.buildkite/scripts/pipelines/pull_request/pipeline.ts index 795be83dd72e79..07bd9472f5242f 100644 --- a/.buildkite/scripts/pipelines/pull_request/pipeline.ts +++ b/.buildkite/scripts/pipelines/pull_request/pipeline.ts @@ -300,20 +300,9 @@ const uploadPipeline = (pipelineContent: string | object) => { } if ( - (await doAnyChangesMatch([ - /^src\/plugins\/controls/, - /^packages\/kbn-securitysolution-.*/, - /^x-pack\/plugins\/lists/, - /^x-pack\/plugins\/security_solution/, - /^x-pack\/plugins\/timelines/, - /^x-pack\/plugins\/triggers_actions_ui\/public\/application\/sections\/action_connector_form/, - /^x-pack\/plugins\/triggers_actions_ui\/public\/application\/sections\/alerts_table/, - /^x-pack\/plugins\/triggers_actions_ui\/public\/application\/context\/connectors_context\.tsx/, - /^x-pack\/test\/defend_workflows_cypress/, - /^x-pack\/test\/security_solution_cypress/, - /^fleet_packages\.json/, // It contains reference to prebuilt detection rules, we want to run security solution tests if it changes - ])) || - GITHUB_PR_LABELS.includes('ci:all-cypress-suites') + ((await doAnyChangesMatch([/^x-pack\/plugins\/osquery/, /^x-pack\/test\/osquery_cypress/])) || + GITHUB_PR_LABELS.includes('ci:all-cypress-suites')) && + !GITHUB_PR_LABELS.includes('ci:skip-cypress-osquery') ) { pipeline.push( getPipeline('.buildkite/pipelines/pull_request/security_solution/osquery_cypress.yml') diff --git a/.buildkite/scripts/steps/cloud/build_and_deploy.sh b/.buildkite/scripts/steps/cloud/build_and_deploy.sh index 62f92b716b6511..15de3aa8614b8a 100755 --- a/.buildkite/scripts/steps/cloud/build_and_deploy.sh +++ b/.buildkite/scripts/steps/cloud/build_and_deploy.sh @@ -86,7 +86,13 @@ if [ -z "${CLOUD_DEPLOYMENT_ID}" ] || [ "${CLOUD_DEPLOYMENT_ID}" = 'null' ]; the VAULT_SECRET_ID="$(retry 5 15 gcloud secrets versions access latest --secret=kibana-buildkite-vault-secret-id)" VAULT_TOKEN=$(retry 5 30 vault write -field=token auth/approle/login role_id="$VAULT_ROLE_ID" secret_id="$VAULT_SECRET_ID") retry 5 30 vault login -no-print "$VAULT_TOKEN" - vault_set "cloud-deploy/$CLOUD_DEPLOYMENT_NAME" username="$CLOUD_DEPLOYMENT_USERNAME" password="$CLOUD_DEPLOYMENT_PASSWORD" + + # TODO: remove after https://github.com/elastic/kibana-operations/issues/15 is done + if [[ "$IS_LEGACY_VAULT_ADDR" == "true" ]]; then + vault_set "cloud-deploy/$CLOUD_DEPLOYMENT_NAME" username="$CLOUD_DEPLOYMENT_USERNAME" password="$CLOUD_DEPLOYMENT_PASSWORD" + else + vault_kv_set "cloud-deploy/$CLOUD_DEPLOYMENT_NAME" username="$CLOUD_DEPLOYMENT_USERNAME" password="$CLOUD_DEPLOYMENT_PASSWORD" + fi echo "Enabling Stack Monitoring..." jq ' @@ -121,10 +127,11 @@ fi CLOUD_DEPLOYMENT_KIBANA_URL=$(ecctl deployment show "$CLOUD_DEPLOYMENT_ID" | jq -r '.resources.kibana[0].info.metadata.aliased_url') CLOUD_DEPLOYMENT_ELASTICSEARCH_URL=$(ecctl deployment show "$CLOUD_DEPLOYMENT_ID" | jq -r '.resources.elasticsearch[0].info.metadata.aliased_url') -if [[ "$VAULT_ADDR" == *"secrets.elastic.co"* ]]; then - VAULT_PATH_PREFIX="secret/kibana-issues/dev" +# TODO: remove after https://github.com/elastic/kibana-operations/issues/15 is done +if [[ "$IS_LEGACY_VAULT_ADDR" == "true" ]]; then + VAULT_READ_COMMAND="vault read $VAULT_PATH_PREFIX/cloud-deploy/$CLOUD_DEPLOYMENT_NAME" else - VAULT_PATH_PREFIX="secret/ci/elastic-kibana" + VAULT_READ_COMMAND="vault kv get $VAULT_KV_PREFIX/cloud-deploy/$CLOUD_DEPLOYMENT_NAME" fi cat << EOF | buildkite-agent annotate --style "info" --context cloud @@ -134,7 +141,7 @@ cat << EOF | buildkite-agent annotate --style "info" --context cloud Elasticsearch: $CLOUD_DEPLOYMENT_ELASTICSEARCH_URL - Credentials: \`vault read $VAULT_PATH_PREFIX/cloud-deploy/$CLOUD_DEPLOYMENT_NAME\` + Credentials: \`$VAULT_READ_COMMAND\` Kibana image: \`$KIBANA_CLOUD_IMAGE\` diff --git a/.buildkite/scripts/steps/serverless/build_and_deploy.sh b/.buildkite/scripts/steps/serverless/build_and_deploy.sh index 3e69f4b4878b76..b195d7ad36ea55 100644 --- a/.buildkite/scripts/steps/serverless/build_and_deploy.sh +++ b/.buildkite/scripts/steps/serverless/build_and_deploy.sh @@ -77,7 +77,14 @@ deploy() { VAULT_SECRET_ID="$(retry 5 15 gcloud secrets versions access latest --secret=kibana-buildkite-vault-secret-id)" VAULT_TOKEN=$(retry 5 30 vault write -field=token auth/approle/login role_id="$VAULT_ROLE_ID" secret_id="$VAULT_SECRET_ID") retry 5 30 vault login -no-print "$VAULT_TOKEN" - vault_set "cloud-deploy/$PROJECT_NAME" username="$PROJECT_USERNAME" password="$PROJECT_PASSWORD" id="$PROJECT_ID" + + # TODO: remove after https://github.com/elastic/kibana-operations/issues/15 is done + if [[ "$IS_LEGACY_VAULT_ADDR" == "true" ]]; then + vault_set "cloud-deploy/$PROJECT_NAME" username="$PROJECT_USERNAME" password="$PROJECT_PASSWORD" id="$PROJECT_ID" + else + vault_kv_set "cloud-deploy/$PROJECT_NAME" username="$PROJECT_USERNAME" password="$PROJECT_PASSWORD" id="$PROJECT_ID" + fi + else echo "Updating project..." curl -s \ @@ -91,10 +98,11 @@ deploy() { PROJECT_KIBANA_LOGIN_URL="${PROJECT_KIBANA_URL}/login" PROJECT_ELASTICSEARCH_URL=$(jq -r --slurp '.[1].endpoints.elasticsearch' $DEPLOY_LOGS) - if [[ "$VAULT_ADDR" == *"secrets.elastic.co"* ]]; then - VAULT_PATH_PREFIX="secret/kibana-issues/dev" + # TODO: remove after https://github.com/elastic/kibana-operations/issues/15 is done + if [[ "$IS_LEGACY_VAULT_ADDR" == "true" ]]; then + VAULT_READ_COMMAND="vault read $VAULT_PATH_PREFIX/cloud-deploy/$PROJECT_NAME" else - VAULT_PATH_PREFIX="secret/ci/elastic-kibana" + VAULT_READ_COMMAND="vault kv get $VAULT_KV_PREFIX/cloud-deploy/$PROJECT_NAME" fi cat << EOF | buildkite-agent annotate --style "info" --context "project-$PROJECT_TYPE" @@ -104,7 +112,7 @@ Kibana: $PROJECT_KIBANA_LOGIN_URL Elasticsearch: $PROJECT_ELASTICSEARCH_URL -Credentials: \`vault read $VAULT_PATH_PREFIX/cloud-deploy/$PROJECT_NAME\` +Credentials: \`$VAULT_READ_COMMAND\` Kibana image: \`$KIBANA_IMAGE\` EOF diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index 5130fad9abad48..0c66deac68c0d9 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -581,7 +581,7 @@ Specifies whether to skip writing alerts and scheduling actions if rule processing was cancelled due to a timeout. Default: `true`. This setting can be overridden by individual rule types. -`xpack.alerting.rules.maxScheduledPerMinute` {ess-icon}:: +`xpack.alerting.rules.maxScheduledPerMinute`:: Specifies the maximum number of rules to run per minute. Default: 10000 `xpack.alerting.rules.minimumScheduleInterval.value` {ess-icon}:: diff --git a/examples/ui_action_examples/public/plugin.ts b/examples/ui_action_examples/public/plugin.ts index a680e3c94b44fc..8b740b556cacc1 100644 --- a/examples/ui_action_examples/public/plugin.ts +++ b/examples/ui_action_examples/public/plugin.ts @@ -33,7 +33,6 @@ export class UiActionExamplesPlugin openModal: (await core.getStartServices())[0].overlays.openModal, })); - uiActions.registerAction(helloWorldAction); uiActions.addTriggerAction(helloWorldTrigger.id, helloWorldAction); } diff --git a/examples/ui_actions_explorer/public/app.tsx b/examples/ui_actions_explorer/public/app.tsx index e267279296324f..31ac9c3c29ed91 100644 --- a/examples/ui_actions_explorer/public/app.tsx +++ b/examples/ui_actions_explorer/public/app.tsx @@ -6,104 +6,50 @@ * Side Public License, v 1. */ -import React, { useState } from 'react'; +import React from 'react'; import ReactDOM from 'react-dom'; import { EuiPage, - EuiButton, EuiPageBody, EuiPageTemplate, EuiPageSection, EuiSpacer, - EuiText, - EuiFieldText, - EuiCallOut, EuiPageHeader, - EuiModalBody, } from '@elastic/eui'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; -import { UiActionsStart, createAction } from '@kbn/ui-actions-plugin/public'; +import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; import { AppMountParameters, OverlayStart } from '@kbn/core/public'; -import { HELLO_WORLD_TRIGGER_ID, ACTION_HELLO_WORLD } from '@kbn/ui-actions-examples-plugin/public'; import { TriggerContextExample } from './trigger_context_example'; import { ContextMenuExamples } from './context_menu_examples'; +import { Overview } from './overview'; +import { HelloWorldExample } from './hello_world_example'; interface Props { - uiActionsApi: UiActionsStart; + uiActionsStartService: UiActionsStart; openModal: OverlayStart['openModal']; } -const ActionsExplorer = ({ uiActionsApi, openModal }: Props) => { - const [name, setName] = useState('Waldo'); - const [confirmationText, setConfirmationText] = useState(''); +const ActionsExplorer = ({ uiActionsStartService, openModal }: Props) => { return ( - + - -

- By default there is a single action attached to the `HELLO_WORLD_TRIGGER`. Clicking - this button will cause it to be executed immediately. -

-
- uiActionsApi.executeTriggerActions(HELLO_WORLD_TRIGGER_ID, {})} - > - Say hello world! - + - -

- Lets dynamically add new actions to this trigger. After you click this button, click - the above button again. This time it should offer you multiple options to choose - from. Using the UI Action and Trigger API makes your plugin extensible by other - plugins. Any actions attached to the `HELLO_WORLD_TRIGGER_ID` will show up here! -

- setName(e.target.value)} /> - { - const dynamicAction = createAction({ - id: `${ACTION_HELLO_WORLD}-${name}`, - type: ACTION_HELLO_WORLD, - getDisplayName: () => `Say hello to ${name}`, - execute: async () => { - const overlay = openModal( - toMountPoint( - - - {`Hello ${name}`} - {' '} - overlay.close()}> - Close - - - ) - ); - }, - }); - uiActionsApi.addTriggerAction(HELLO_WORLD_TRIGGER_ID, dynamicAction); - setConfirmationText( - `You've successfully added a new action: ${dynamicAction.getDisplayName({ - trigger: uiActionsApi.getTrigger(HELLO_WORLD_TRIGGER_ID), - })}. Refresh the page to reset state. It's up to the user of the system to persist state like this.` - ); - }} - > - Say hello to me! - - {confirmationText !== '' ? {confirmationText} : undefined} -
+ + + - + diff --git a/examples/ui_actions_explorer/public/hello_world_example.tsx b/examples/ui_actions_explorer/public/hello_world_example.tsx new file mode 100644 index 00000000000000..14fa8cae326066 --- /dev/null +++ b/examples/ui_actions_explorer/public/hello_world_example.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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, { useState } from 'react'; + +import { EuiButton, EuiSpacer, EuiText, EuiModalBody, EuiLink, EuiSwitch } from '@elastic/eui'; +import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { UiActionsStart, createAction } from '@kbn/ui-actions-plugin/public'; +import { OverlayStart } from '@kbn/core/public'; +import { HELLO_WORLD_TRIGGER_ID, ACTION_HELLO_WORLD } from '@kbn/ui-actions-examples-plugin/public'; + +const DYNAMIC_ACTION_ID = `${ACTION_HELLO_WORLD}-Waldo`; + +interface Props { + uiActionsStartService: UiActionsStart; + openModal: OverlayStart['openModal']; +} + +export const HelloWorldExample = ({ uiActionsStartService, openModal }: Props) => { + const [isChecked, setIsChecked] = useState(false); + + const actionsMessage = isChecked ? '2 actions attached' : '1 action attached'; + + return ( + <> + +

Hello world example

+

+ The{' '} + + ui_action_example plugin + {' '} + registers the HELLO_WORLD_TRIGGER_ID trigger and attaches the{' '} + ACTION_HELLO_WORLD action to the trigger. The ACTION_HELLO_WORLD opens a + modal when executed. Fire the "Hello world" event by clicking the button below. +

+
+ uiActionsStartService.getTrigger(HELLO_WORLD_TRIGGER_ID).exec({})} + > + Click me to fire "Hello world" event ({actionsMessage}) + + + + + +

+ You can dynamically add a new action to a trigger. Click the switch below to attach a + second action to HELLO_WORLD_TRIGGER_ID trigger. What do you think will happen + when you click the button and the trigger has multiple actions? +

+ { + setIsChecked(e.target.checked); + if (e.target.checked) { + const dynamicAction = createAction({ + id: DYNAMIC_ACTION_ID, + type: ACTION_HELLO_WORLD, + getDisplayName: () => 'Say hello to Waldo', + execute: async () => { + const overlay = openModal( + toMountPoint( + + Hello Waldo{' '} + overlay.close()}> + Close + + + ) + ); + }, + }); + uiActionsStartService.addTriggerAction(HELLO_WORLD_TRIGGER_ID, dynamicAction); + } else { + uiActionsStartService.detachAction(HELLO_WORLD_TRIGGER_ID, DYNAMIC_ACTION_ID); + } + }} + /> +
+ + ); +}; diff --git a/examples/ui_actions_explorer/public/overview.tsx b/examples/ui_actions_explorer/public/overview.tsx new file mode 100644 index 00000000000000..6b5014111cbdf4 --- /dev/null +++ b/examples/ui_actions_explorer/public/overview.tsx @@ -0,0 +1,55 @@ +/* + * 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 { EuiText, EuiLink } from '@elastic/eui'; + +export const Overview = () => { + return ( + +

Overview

+

+ Actions and triggers are an event system abstraction that decouples firing an event and + responding to an event. +

+ Key concepts: +
    +
  • + Trigger is an id that represents an event type, for example + PANEL_CLICK. +
  • +
  • + Action is a{' '} + + class + {' '} + that responds to an event. Multiple actions can be registered to an event type. Actions + can respond to multiple event types. +
  • +
  • + Context is runtime state passed between an event and the responder of an + event. +
  • +
+

+ The purpose for the event system abstraction is to make event handling extensible, allowing + plugins to register their own event types and responses and register responses to existing + event types. +

+

+ Use triggers to make your plugin extensible. For example, your plugin could register a + trigger. Then, other plugins can extend your plugin by registering new actions for the + trigger. Finally, when the trigger is fired, all attached actions are available. +

+
+ ); +}; diff --git a/examples/ui_actions_explorer/public/plugin.tsx b/examples/ui_actions_explorer/public/plugin.tsx index 045ae2f383ee9e..369c827f3af973 100644 --- a/examples/ui_actions_explorer/public/plugin.tsx +++ b/examples/ui_actions_explorer/public/plugin.tsx @@ -69,7 +69,7 @@ export class UiActionsExplorerPlugin implements Plugin = Object.keys(reducers).reduce( (acc, key: keyof ReducerType) => { const sliceAction = - slice.actions[key as keyof CaseReducerActions>]; + slice.actions[key as keyof CaseReducerActions, string>]; acc[key] = (payload) => store.dispatch(sliceAction(payload)); return acc; }, diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/legacy/table_cell_actions.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/legacy/table_cell_actions.tsx index 2a202513340bf0..0b187c6d5b884f 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/legacy/table_cell_actions.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/legacy/table_cell_actions.tsx @@ -35,24 +35,30 @@ export const TableActions = ({ }: TableActionsProps) => { return (
- onFilter(fieldMapping, flattenedField, '+')} - /> - onFilter(fieldMapping, flattenedField, '-')} - /> + {onFilter && ( + onFilter(fieldMapping, flattenedField, '+')} + /> + )} + {onFilter && ( + onFilter(fieldMapping, flattenedField, '-')} + /> + )} onToggleColumn(field)} /> - onFilter('_exists_', field, '+')} - scripted={fieldMapping && fieldMapping.scripted} - /> + {onFilter && ( + onFilter('_exists_', field, '+')} + scripted={fieldMapping && fieldMapping.scripted} + /> + )}
); }; diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.tsx index 5c6cbc6d806522..b8e5b9cd379708 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.tsx @@ -287,7 +287,7 @@ export const DocViewerTable = ({ diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.test.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.test.tsx index efcbc7c28958ed..076ca2d67be105 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.test.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.test.tsx @@ -25,9 +25,9 @@ describe('TableActions', () => { onTogglePinned={jest.fn()} /> ); - expect(screen.getByTestId('addFilterForValueButton-message')).toBeDisabled(); - expect(screen.getByTestId('addFilterOutValueButton-message')).toBeDisabled(); - expect(screen.getByTestId('addExistsFilterButton-message')).toBeDisabled(); + expect(screen.queryByTestId('addFilterForValueButton-message')).not.toBeInTheDocument(); + expect(screen.queryByTestId('addFilterOutValueButton-message')).not.toBeInTheDocument(); + expect(screen.queryByTestId('addExistsFilterButton-message')).not.toBeInTheDocument(); expect(screen.getByTestId('toggleColumnButton-message')).not.toBeDisabled(); expect(screen.getByTestId('togglePinFilterButton-message')).not.toBeDisabled(); }); diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx index 0ebef67a7c76b6..cfbfd1b1cde031 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx @@ -31,6 +31,16 @@ interface TableActionsProps { onTogglePinned: (field: string) => void; } +interface PanelItem { + name: string; + 'aria-label': string; + toolTipContent?: string; + disabled?: boolean; + 'data-test-subj': string; + icon: string; + onClick: () => void; +} + export const TableActions = ({ mode = 'as_popover', pinned, @@ -51,8 +61,7 @@ export const TableActions = ({ }); // Filters pair - const filtersPairDisabled = - !fieldMapping || !fieldMapping.filterable || ignoredValue || !onFilter; + const filtersPairDisabled = !fieldMapping || !fieldMapping.filterable || ignoredValue; const filterAddLabel = i18n.translate( 'unifiedDocViewer.docViews.table.filterForValueButtonTooltip', { @@ -89,7 +98,7 @@ export const TableActions = ({ 'unifiedDocViewer.docViews.table.filterForFieldPresentButtonAriaLabel', { defaultMessage: 'Filter for field present' } ); - const filtersExistsDisabled = !fieldMapping || !fieldMapping.filterable || !onFilter; + const filtersExistsDisabled = !fieldMapping || !fieldMapping.filterable; const filtersExistsToolTip = (filtersExistsDisabled && (fieldMapping && fieldMapping.scripted @@ -145,57 +154,61 @@ export const TableActions = ({ [closePopover] ); + let panelItems: PanelItem[] = [ + { + name: toggleColumnsLabel, + 'aria-label': toggleColumnsAriaLabel, + 'data-test-subj': `toggleColumnButton-${field}`, + icon: 'listAdd', + onClick: onClickAction(onToggleColumn.bind({}, field)), + }, + { + name: pinnedLabel, + 'aria-label': pinnedAriaLabel, + icon: pinnedIconType, + 'data-test-subj': `togglePinFilterButton-${field}`, + onClick: onClickAction(togglePinned), + }, + ]; + + if (onFilter) { + panelItems = [ + { + name: filterAddLabel, + 'aria-label': filterAddAriaLabel, + toolTipContent: filtersPairToolTip, + icon: 'plusInCircle', + disabled: filtersPairDisabled, + 'data-test-subj': `addFilterForValueButton-${field}`, + onClick: onClickAction(onFilter.bind({}, fieldMapping, flattenedField, '+')), + }, + { + name: filterOutLabel, + 'aria-label': filterOutAriaLabel, + toolTipContent: filtersPairToolTip, + icon: 'minusInCircle', + disabled: filtersPairDisabled, + 'data-test-subj': `addFilterOutValueButton-${field}`, + onClick: onClickAction(onFilter.bind({}, fieldMapping, flattenedField, '-')), + }, + { + name: filterExistsLabel, + 'aria-label': filterExistsAriaLabel, + toolTipContent: filtersExistsToolTip, + icon: 'filter', + disabled: filtersExistsDisabled, + 'data-test-subj': `addExistsFilterButton-${field}`, + onClick: onClickAction(onFilter.bind({}, '_exists_', field, '+')), + }, + ...panelItems, + ]; + } + const panels = [ { id: 0, title: actionsLabel, - items: [ - { - name: filterAddLabel, - 'aria-label': filterAddAriaLabel, - toolTipContent: filtersPairToolTip, - icon: 'plusInCircle', - disabled: filtersPairDisabled, - 'data-test-subj': `addFilterForValueButton-${field}`, - onClick: onFilter - ? onClickAction(onFilter.bind({}, fieldMapping, flattenedField, '+')) - : undefined, - }, - { - name: filterOutLabel, - 'aria-label': filterOutAriaLabel, - toolTipContent: filtersPairToolTip, - icon: 'minusInCircle', - disabled: filtersPairDisabled, - 'data-test-subj': `addFilterOutValueButton-${field}`, - onClick: onFilter - ? onClickAction(onFilter.bind({}, fieldMapping, flattenedField, '-')) - : undefined, - }, - { - name: filterExistsLabel, - 'aria-label': filterExistsAriaLabel, - toolTipContent: filtersExistsToolTip, - icon: 'filter', - disabled: filtersExistsDisabled, - 'data-test-subj': `addExistsFilterButton-${field}`, - onClick: onFilter ? onClickAction(onFilter.bind({}, '_exists_', field, '+')) : undefined, - }, - { - name: toggleColumnsLabel, - 'aria-label': toggleColumnsAriaLabel, - 'data-test-subj': `toggleColumnButton-${field}`, - icon: 'listAdd', - onClick: onClickAction(onToggleColumn.bind({}, field)), - }, - { - name: pinnedLabel, - 'aria-label': pinnedAriaLabel, - icon: pinnedIconType, - 'data-test-subj': `togglePinFilterButton-${field}`, - onClick: onClickAction(togglePinned), - }, - ], + items: panelItems, }, ]; diff --git a/test/tsconfig.json b/test/tsconfig.json index d451e64e6329bb..d39f26afc61fa1 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -18,7 +18,7 @@ "api_integration/apis/logstash/pipelines/fixtures/*.json", "api_integration/apis/telemetry/fixtures/*.json", "api_integration/apis/telemetry/fixtures/*.json" - ], + , "../x-pack/test_serverless/functional/test_suites/common/saved_objects_management/export_transform copy.ts" ], "exclude": ["target/**/*", "*/plugins/**/*", "plugins/**/*"], "kbn_references": [ "@kbn/core", diff --git a/x-pack/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/settings_page/settings_page.tsx b/x-pack/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/settings_page/settings_page.tsx index d651957f0771af..e66bb9e17690d2 100644 --- a/x-pack/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/settings_page/settings_page.tsx +++ b/x-pack/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/settings_page/settings_page.tsx @@ -17,7 +17,10 @@ import { EuiStat, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useUiTracker } from '@kbn/observability-shared-plugin/public'; +import { + BottomBarActions, + useUiTracker, +} from '@kbn/observability-shared-plugin/public'; import React, { useMemo, useState } from 'react'; import { useHistory } from 'react-router-dom'; import { getOptionLabel } from '../../../../../../../common/agent_configuration/all_option'; @@ -30,7 +33,6 @@ import { import { AgentName } from '../../../../../../../typings/es_schemas/ui/fields/agent'; import { useApmPluginContext } from '../../../../../../context/apm_plugin/use_apm_plugin_context'; import { FETCH_STATUS } from '../../../../../../hooks/use_fetcher'; -import { BottomBarActions } from '../../../bottom_bar_actions'; import { saveConfig } from './save_config'; import { SettingFormRow } from './setting_form_row'; @@ -200,6 +202,7 @@ export function SettingsPage({ { defaultMessage: 'Save configuration' } )} unsavedChangesCount={unsavedChangesCount} + appTestSubj="apm" /> )} diff --git a/x-pack/plugins/apm/public/components/app/settings/general_settings/index.tsx b/x-pack/plugins/apm/public/components/app/settings/general_settings/index.tsx index 797dcc910a3907..b03f2e98a4fb6e 100644 --- a/x-pack/plugins/apm/public/components/app/settings/general_settings/index.tsx +++ b/x-pack/plugins/apm/public/components/app/settings/general_settings/index.tsx @@ -25,13 +25,13 @@ import { import { isEmpty } from 'lodash'; import React from 'react'; import { + BottomBarActions, useEditableSettings, useUiTracker, } from '@kbn/observability-shared-plugin/public'; import { FieldRowProvider } from '@kbn/management-settings-components-field-row'; import { ValueValidation } from '@kbn/core-ui-settings-browser/src/types'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; -import { BottomBarActions } from '../bottom_bar_actions'; const LazyFieldRow = React.lazy(async () => ({ default: (await import('@kbn/management-settings-components-field-row')) @@ -126,6 +126,7 @@ export function GeneralSettings() { defaultMessage: 'Save changes', })} unsavedChangesCount={Object.keys(unsavedChanges).length} + appTestSubj="apm" /> )} diff --git a/x-pack/plugins/cases/public/components/create/description.test.tsx b/x-pack/plugins/cases/public/components/create/description.test.tsx index d7cef68771739a..871f319ad81004 100644 --- a/x-pack/plugins/cases/public/components/create/description.test.tsx +++ b/x-pack/plugins/cases/public/components/create/description.test.tsx @@ -21,6 +21,7 @@ import { MAX_DESCRIPTION_LENGTH } from '../../../common/constants'; // FLAKY: https://github.com/elastic/kibana/issues/174133 // FLAKY: https://github.com/elastic/kibana/issues/174134 // FLAKY: https://github.com/elastic/kibana/issues/174135 +// FLAKY: https://github.com/elastic/kibana/issues/175204 describe.skip('Description', () => { let globalForm: FormHook; let appMockRender: AppMockRenderer; diff --git a/x-pack/plugins/data_visualizer/kibana.jsonc b/x-pack/plugins/data_visualizer/kibana.jsonc index 9400df0f7c1743..79a1e1fedacaf0 100644 --- a/x-pack/plugins/data_visualizer/kibana.jsonc +++ b/x-pack/plugins/data_visualizer/kibana.jsonc @@ -17,7 +17,8 @@ "uiActions", "charts", "unifiedSearch", - "savedSearch" + "savedSearch", + "fieldFormats" ], "optionalPlugins": [ "security", diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/results_links/results_links.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/results_links/results_links.tsx index 7bb835d292e33d..0c46eb865d8fad 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/results_links/results_links.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/results_links/results_links.tsx @@ -209,6 +209,7 @@ export const ResultsLinks: FC = ({ {createDataView && discoverLink && ( } title={ = ({ {indexManagementLink && ( } title={ = ({ {dataViewsManagementLink && ( } title={ = ({ )} } data-test-subj="fileDataVisFilebeatConfigLink" title={ @@ -271,6 +275,7 @@ export const ResultsLinks: FC = ({ asyncHrefCards.map((link) => ( } data-test-subj="fileDataVisLink" title={link.title} diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/doc_count_chart/axes.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/doc_count_chart/axes.tsx new file mode 100644 index 00000000000000..173923ca282c4b --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/doc_count_chart/axes.tsx @@ -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 React, { FC } from 'react'; +import { Axis, Position } from '@elastic/charts'; +import { MULTILAYER_TIME_AXIS_STYLE } from '@kbn/charts-plugin/common'; +import type { LineChartPoint } from './event_rate_chart'; +import { useDataVisualizerKibana } from '../../../kibana_context'; + +interface Props { + chartData?: LineChartPoint[]; +} + +// round to 2dp +function tickFormatter(d: number): string { + return (Math.round(d * 100) / 100).toString(); +} + +export const Axes: FC = ({ chartData }) => { + const yDomain = getYRange(chartData); + const { + services: { fieldFormats, uiSettings }, + } = useDataVisualizerKibana(); + const useLegacyTimeAxis = uiSettings.get('visualization:useLegacyTimeAxis', false); + const xAxisFormatter = fieldFormats.deserialize({ id: 'date' }); + + return ( + <> + xAxisFormatter.convert(value)} + labelFormat={useLegacyTimeAxis ? undefined : () => ''} + timeAxisLayerCount={useLegacyTimeAxis ? 0 : 2} + style={useLegacyTimeAxis ? {} : MULTILAYER_TIME_AXIS_STYLE} + /> + + + ); +}; + +function getYRange(chartData?: LineChartPoint[]) { + const fit = false; + + if (chartData === undefined) { + return { fit, min: NaN, max: NaN }; + } + + if (chartData.length === 0) { + return { min: 0, max: 0, fit }; + } + + let max: number = Number.MIN_VALUE; + let min: number = Number.MAX_VALUE; + chartData.forEach((r) => { + max = Math.max(r.value, max); + min = Math.min(r.value, min); + }); + + const padding = (max - min) * 0.1; + max += padding; + min -= padding; + + return { + min, + max, + fit, + }; +} diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/doc_count_chart/doc_count_chart.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/doc_count_chart/doc_count_chart.tsx new file mode 100644 index 00000000000000..5d6e3c0b9ca020 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/doc_count_chart/doc_count_chart.tsx @@ -0,0 +1,211 @@ +/* + * 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, { FC, useEffect, useState, useCallback, useRef, useMemo } from 'react'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { IImporter } from '@kbn/file-upload-plugin/public'; +import moment, { type Moment } from 'moment'; +import { useTimeBuckets } from '../../../common/hooks/use_time_buckets'; +import { IMPORT_STATUS, type Statuses } from '../import_progress'; +import { EventRateChart, type LineChartPoint } from './event_rate_chart'; +import { runDocCountSearch } from './doc_count_search'; + +const BAR_TARGET = 150; +const PROGRESS_INCREMENT = 5; +const FINISHED_CHECKS = 3; +const ERROR_ATTEMPTS = 3; +const BACK_FILL_BUCKETS = 8; + +export const DocCountChart: FC<{ + statuses: Statuses; + dataStart: DataPublicPluginStart; + importer: IImporter; +}> = ({ statuses, dataStart, importer }) => { + const timeBuckets = useTimeBuckets(); + const index = useMemo(() => importer.getIndex(), [importer]); + const timeField = useMemo(() => importer.getTimeField(), [importer]); + + const [loading, setLoading] = useState(false); + const [loadingTimeRange, setLoadingTimeRange] = useState(false); + const [finished, setFinished] = useState(false); + const [previousProgress, setPreviousProgress] = useState(0); + const [lastNonZeroTimeMs, setLastNonZeroTimeMs] = useState< + { index: number; time: number } | undefined + >(undefined); + + const [eventRateChartData, setEventRateChartData] = useState([]); + const [timeRange, setTimeRange] = useState<{ start: Moment; end: Moment } | undefined>(undefined); + + const loadFullData = useRef(false); + + const [errorAttempts, setErrorAttempts] = useState(ERROR_ATTEMPTS); + const recordFailure = useCallback(() => { + setErrorAttempts(errorAttempts - 1); + }, [errorAttempts]); + + const loadData = useCallback(async () => { + if (timeField === undefined || index === undefined || timeRange === undefined) { + return; + } + + setLoading(true); + timeBuckets.setInterval('auto'); + + const { start, end } = timeRange; + const fullData = loadFullData.current; + + try { + const startMs = + fullData === true || lastNonZeroTimeMs === undefined + ? start.valueOf() + : lastNonZeroTimeMs.time; + const endMs = end.valueOf(); + + if (start != null && end != null) { + timeBuckets.setBounds({ + min: start, + max: end, + }); + timeBuckets.setBarTarget(BAR_TARGET); + } + + const data = await runDocCountSearch( + dataStart, + index, + timeField, + startMs, + endMs, + timeBuckets + ); + + const newData = + fullData === true + ? data + : [...eventRateChartData].splice(0, lastNonZeroTimeMs?.index ?? 0).concat(data); + + setEventRateChartData(newData); + setLastNonZeroTimeMs(findLastTimestamp(newData, BACK_FILL_BUCKETS)); + } catch (error) { + recordFailure(); + } + setLoading(false); + }, [ + timeField, + index, + timeRange, + timeBuckets, + lastNonZeroTimeMs, + dataStart, + eventRateChartData, + recordFailure, + ]); + + const finishedChecks = useCallback( + async (counter: number) => { + loadData(); + if (counter !== 0) { + setTimeout(() => { + finishedChecks(counter - 1); + }, 2 * 1000); + } + }, + [loadData] + ); + + const loadTimeRange = useCallback(async () => { + if (loadingTimeRange === true) { + return; + } + setLoadingTimeRange(true); + try { + const { start, end } = await importer.previewIndexTimeRange(); + if (start === null || end === null || start >= end) { + throw new Error('Invalid time range'); + } + setTimeRange({ start: moment(start), end: moment(end) }); + } catch (error) { + recordFailure(); + } + setLoadingTimeRange(false); + }, [importer, loadingTimeRange, recordFailure]); + + useEffect( + function loadProgress() { + if (errorAttempts === 0) { + return; + } + + if (timeRange === undefined) { + loadTimeRange(); + return; + } + + if (loading === false && statuses.uploadProgress > 1 && statuses.uploadProgress < 100) { + if (statuses.uploadProgress - previousProgress > PROGRESS_INCREMENT) { + setPreviousProgress(statuses.uploadProgress); + + loadData(); + } + } else if (loading === false && statuses.uploadProgress === 100 && finished === false) { + setFinished(true); + finishedChecks(FINISHED_CHECKS); + loadFullData.current = true; + } + }, + [ + finished, + finishedChecks, + loadData, + loadTimeRange, + loading, + loadingTimeRange, + previousProgress, + statuses, + timeRange, + errorAttempts, + ] + ); + + if ( + timeField === undefined || + statuses.indexCreatedStatus === IMPORT_STATUS.INCOMPLETE || + statuses.ingestPipelineCreatedStatus === IMPORT_STATUS.INCOMPLETE || + errorAttempts === 0 || + eventRateChartData.length === 0 + ) { + return null; + } + + return ( + <> + + + ); +}; + +/** + * Finds the last non-zero data point in the chart data + * backFillOffset can be set to jump back a number of buckets from the final non-zero bucket. + * This means the next time we load data, refresh the last n buckets of data in case there are new documents. + * @param data LineChartPoint[] + * @param backFillOffset number + * @returns + */ +function findLastTimestamp(data: LineChartPoint[], backFillOffset = 0) { + let lastNonZeroDataPoint = data[0].time; + let index = 0; + for (let i = 0; i < data.length; i++) { + if (data[i].value > 0) { + const backTrackIndex = i - backFillOffset >= 0 ? i - backFillOffset : i; + lastNonZeroDataPoint = data[backTrackIndex].time; + index = backTrackIndex; + } else { + break; + } + } + return { index, time: lastNonZeroDataPoint as number }; +} diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/doc_count_chart/doc_count_search.ts b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/doc_count_chart/doc_count_search.ts new file mode 100644 index 00000000000000..3772a9b116c567 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/doc_count_chart/doc_count_search.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { lastValueFrom } from 'rxjs'; +import type { DataPublicPluginStart, IKibanaSearchResponse } from '@kbn/data-plugin/public'; +import type { LineChartPoint } from './event_rate_chart'; +import type { TimeBuckets } from '../../../../../common/services/time_buckets'; + +type EventRateResponse = IKibanaSearchResponse< + estypes.SearchResponse< + unknown, + { + eventRate: { + buckets: Array<{ key: number; doc_count: number }>; + }; + } + > +>; + +export async function runDocCountSearch( + dataStart: DataPublicPluginStart, + index: string, + timeField: string, + earliestMs: number, + latestMs: number, + timeBuckets: TimeBuckets +): Promise { + const intervalMs = timeBuckets.getInterval().asMilliseconds(); + const resp = await lastValueFrom( + dataStart.search.search({ + params: { + index, + body: { + size: 0, + query: { + bool: { + must: [ + { + range: { + [timeField]: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, + }, + }, + { + match_all: {}, + }, + ], + }, + }, + aggs: { + eventRate: { + date_histogram: { + field: timeField, + fixed_interval: `${intervalMs}ms`, + min_doc_count: 0, + extended_bounds: { + min: earliestMs, + max: latestMs, + }, + }, + }, + }, + }, + }, + }) + ); + + if (resp.rawResponse.aggregations === undefined) { + return []; + } + + return resp.rawResponse.aggregations.eventRate.buckets.map((b) => ({ + time: b.key, + value: b.doc_count, + })); +} diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/doc_count_chart/event_rate_chart.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/doc_count_chart/event_rate_chart.tsx new file mode 100644 index 00000000000000..5263eb709c2faa --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/doc_count_chart/event_rate_chart.tsx @@ -0,0 +1,77 @@ +/* + * 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, { FC } from 'react'; +import { + HistogramBarSeries, + Chart, + ScaleType, + Settings, + PartialTheme, + Tooltip, + TooltipType, +} from '@elastic/charts'; +import { i18n } from '@kbn/i18n'; +import { euiLightVars } from '@kbn/ui-theme'; +import { Axes } from './axes'; +import { useCurrentEuiTheme } from '../../../common/hooks/use_current_eui_theme'; + +export interface LineChartPoint { + time: number | string; + value: number; +} + +interface Props { + eventRateChartData: LineChartPoint[]; + height: string; + width: string; +} + +export const EventRateChart: FC = ({ eventRateChartData, height, width }) => { + const { euiColorLightShade } = useCurrentEuiTheme(); + const theme: PartialTheme = { + scales: { histogramPadding: 0.2 }, + background: { + color: 'transparent', + }, + axes: { + gridLine: { + horizontal: { + stroke: euiColorLightShade, + }, + vertical: { + stroke: euiColorLightShade, + }, + }, + }, + }; + + return ( +
+ + + + + + + +
+ ); +}; diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/doc_count_chart/index.ts b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/doc_count_chart/index.ts new file mode 100644 index 00000000000000..5944275b19a142 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/doc_count_chart/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 { DocCountChart } from './doc_count_chart'; diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_data_visualizer_view.js b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_data_visualizer_view.js index c4ff7d9c042efd..6e8b6640d854dd 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_data_visualizer_view.js +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_data_visualizer_view.js @@ -342,6 +342,7 @@ export class FileDataVisualizerView extends Component { fileContents={fileContents} data={data} dataViewsContract={this.props.dataViewsContract} + dataStart={this.props.dataStart} fileUpload={this.props.fileUpload} getAdditionalLinks={this.props.getAdditionalLinks} capabilities={this.props.capabilities} diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_summary/failures.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_summary/failures.tsx index fe9d23d3b4c12c..0b6eb59622f06d 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_summary/failures.tsx +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_summary/failures.tsx @@ -34,7 +34,7 @@ export class Failures extends Component { _renderPaginationControl() { return this.props.failedDocs.length > PAGE_SIZE ? ( this.setState({ page })} compressed @@ -43,9 +43,8 @@ export class Failures extends Component { } render() { - const lastDocIndex = this.props.failedDocs.length - 1; const startIndex = this.state.page * PAGE_SIZE; - const endIndex = startIndex + PAGE_SIZE > lastDocIndex ? lastDocIndex : startIndex + PAGE_SIZE; + const endIndex = startIndex + PAGE_SIZE; return ( - - -

- -

-
+ {initialized === false ? ( + + +

+ +

+
+ + + + - - - - - {(initialized === false || importing === true) && ( - )} - - {initialized === true && importing === false && ( - - - - - - - - this.props.onChangeMode(DATAVISUALIZER_MODE.READ)} - isDisabled={importing} - > - - - - - this.props.onCancel()} isDisabled={importing}> - - - - - )} -
+
+ ) : null} {initialized === true && ( - + + {importer !== undefined && importer.initialized() && ( + + )} + {imported === true && ( @@ -615,6 +591,27 @@ export class ImportView extends Component { + + + + + + + + this.props.onCancel()} isDisabled={importing}> + + + + + + + = ({ getAdditionalLinks }) => { const coreStart = getCoreStart(); - const { data, maps, embeddable, discover, share, security, fileUpload, cloud } = + const { data, maps, embeddable, discover, share, security, fileUpload, cloud, fieldFormats } = getPluginsStart(); const services = { data, @@ -30,6 +30,7 @@ export const FileDataVisualizer: FC = ({ getAdditionalLinks }) => { share, security, fileUpload, + fieldFormats, ...coreStart, }; @@ -42,6 +43,7 @@ export const FileDataVisualizer: FC = ({ getAdditionalLinks }) => { { return [ + INGESTION_METHOD_IDS.API, ...(productFeatures.hasWebCrawler ? [INGESTION_METHOD_IDS.CRAWLER] : []), ...(productFeatures.hasConnectors ? [INGESTION_METHOD_IDS.CONNECTOR] : []), - INGESTION_METHOD_IDS.API, ]; }; @@ -40,7 +40,6 @@ export const NewIndex: React.FC = () => { const availableIngestionMethodOptions = getAvailableMethodOptions(productFeatures); const { errorConnectingMessage } = useValues(HttpLogic); - const [selectedMethod, setSelectedMethod] = useState(''); return ( { )} type={type} onSelect={() => { - setSelectedMethod(type); if (type === INGESTION_METHOD_IDS.CONNECTOR) { KibanaLogic.values.navigateToUrl(NEW_INDEX_SELECT_CONNECTOR_PATH); } else { @@ -84,7 +82,6 @@ export const NewIndex: React.FC = () => { ); } }} - isSelected={selectedMethod === type} />
))} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/new_index_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/new_index_card.tsx index 93044cb4e05510..9676394b291056 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/new_index_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/new_index_card.tsx @@ -5,33 +5,35 @@ * 2.0. */ -import React, { MouseEventHandler } from 'react'; +import React from 'react'; -import { EuiCardProps, EuiIconProps, EuiTextColor } from '@elastic/eui'; -import { EuiBadge, EuiButton, EuiCard, EuiIcon, EuiSpacer } from '@elastic/eui'; +import { EuiIconProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { INGESTION_METHOD_IDS } from '../../../../../common/constants'; -import { getIngestionMethodIconType } from './utils'; +import { IngestionCard } from '../shared/ingestion_card/ingestion_card'; + +import { getIngestionMethodButtonIcon, getIngestionMethodIconType } from './utils'; export interface NewIndexCardProps { disabled: boolean; - isSelected?: boolean; - onSelect?: MouseEventHandler; + onSelect?: () => void; type: INGESTION_METHOD_IDS; } export interface MethodCardOptions { - description: EuiCardProps['description']; + buttonIcon: EuiIconProps['type']; + description: string; footer: Record; icon: EuiIconProps['type']; - title: EuiCardProps['title']; + title: string; } const METHOD_CARD_OPTIONS: Record = { [INGESTION_METHOD_IDS.CRAWLER]: { + buttonIcon: getIngestionMethodButtonIcon(INGESTION_METHOD_IDS.CRAWLER), description: i18n.translate( 'xpack.enterpriseSearch.content.newIndex.methodCard.crawler.description', { @@ -43,7 +45,7 @@ const METHOD_CARD_OPTIONS: Record = { buttonLabel: i18n.translate( 'xpack.enterpriseSearch.content.newIndex.methodCard.crawler.label', { - defaultMessage: 'Use a web crawler', + defaultMessage: 'Crawl URL', } ), label: i18n.translate( @@ -59,36 +61,37 @@ const METHOD_CARD_OPTIONS: Record = { }), }, [INGESTION_METHOD_IDS.CONNECTOR]: { + buttonIcon: getIngestionMethodButtonIcon(INGESTION_METHOD_IDS.CONNECTOR), description: i18n.translate( 'xpack.enterpriseSearch.content.newIndex.methodCard.connector.description', { - defaultMessage: - 'Extract, transform, index and sync data from a data source via native or customized connectors', + defaultMessage: 'Extract, transform, index and sync data from a third-party data source', } ), footer: { buttonLabel: i18n.translate( 'xpack.enterpriseSearch.content.newIndex.methodCard.connector.label', { - defaultMessage: 'Use a connector', + defaultMessage: 'Choose a source connector', } ), }, icon: getIngestionMethodIconType(INGESTION_METHOD_IDS.CONNECTOR), title: i18n.translate('xpack.enterpriseSearch.content.newIndex.methodCard.connector.title', { - defaultMessage: 'Connector', + defaultMessage: 'Connectors', }), }, [INGESTION_METHOD_IDS.API]: { + buttonIcon: getIngestionMethodButtonIcon(INGESTION_METHOD_IDS.API), description: i18n.translate( 'xpack.enterpriseSearch.content.newIndex.methodCard.api.description', { - defaultMessage: 'Add documents programmatically by connecting with the API', + defaultMessage: 'Use the API to connect directly to your Elasticsearch index endpoint.', } ), footer: { buttonLabel: i18n.translate('xpack.enterpriseSearch.content.newIndex.methodCard.api.label', { - defaultMessage: 'Use the API', + defaultMessage: 'Create API Index', }), }, icon: getIngestionMethodIconType(INGESTION_METHOD_IDS.API), @@ -97,45 +100,23 @@ const METHOD_CARD_OPTIONS: Record = { }), }, }; -export const NewIndexCard: React.FC = ({ - disabled, - onSelect, - isSelected, - type, -}) => { + +export const NewIndexCard: React.FC = ({ disabled, onSelect, type }) => { if (!METHOD_CARD_OPTIONS[type]) { return null; } - const { icon, title, description, footer } = METHOD_CARD_OPTIONS[type]; + const { buttonIcon, icon, title, description, footer } = METHOD_CARD_OPTIONS[type]; return ( - } + logo={icon} + buttonIcon={buttonIcon} + buttonLabel={footer.buttonLabel} title={title} - description={{description}} - footer={ - <> - {footer.label && ( - <> - {footer.label} - - - )} - - {footer.buttonLabel} - - - } + description={description} + onClick={onSelect} /> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/utils.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/utils.ts index e4bdceb5dcbdf0..63e2bb0ce49ca2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/utils.ts @@ -7,9 +7,11 @@ import { INGESTION_METHOD_IDS } from '../../../../../common/constants'; -import connectorLogo from '../../../../assets/source_icons/connector.svg'; - -import crawlerLogo from '../../../../assets/source_icons/crawler.svg'; +import apiLogo from '../../../../assets/images/api_image.png'; +import connectorLogo from '../../../../assets/images/search_connector.svg'; +import crawlerLogo from '../../../../assets/images/search_crawler.svg'; +import { ConnectorIcon } from '../../../shared/icons/connector'; +import { CrawlerIcon } from '../../../shared/icons/crawler'; import { UNIVERSAL_LANGUAGE_VALUE } from './constants'; import { LanguageForOptimization } from './types'; @@ -26,6 +28,17 @@ export function getIngestionMethodIconType(type: string): string { case INGESTION_METHOD_IDS.CONNECTOR: return connectorLogo; default: - return 'consoleApp'; + return apiLogo; + } +} + +export function getIngestionMethodButtonIcon(type: string): React.FC | string { + switch (type) { + case INGESTION_METHOD_IDS.CRAWLER: + return CrawlerIcon; + case INGESTION_METHOD_IDS.CONNECTOR: + return ConnectorIcon; + default: + return 'console'; } } diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/ingestion_card/ingestion_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/ingestion_card/ingestion_card.tsx new file mode 100644 index 00000000000000..819b46f6b393af --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/ingestion_card/ingestion_card.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { + EuiButton, + EuiCard, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiSpacer, + EuiText, + IconType, +} from '@elastic/eui'; + +import { EuiLinkTo } from '../../../../shared/react_router_helpers'; + +interface IngestionCardProps { + buttonIcon: IconType; + buttonLabel: string; + description: string; + href?: string; + isDisabled?: boolean; + logo: IconType; + onClick?: () => void; + title: string; +} + +export const IngestionCard: React.FC = ({ + buttonIcon, + buttonLabel, + description, + href, + isDisabled, + logo, + onClick, + title, +}) => { + return ( + + + + + + {title} + + + + } + description={ + + {description} + + } + footer={ + onClick ? ( + + {buttonLabel} + + ) : ( + + + {buttonLabel} + + + ) + } + /> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/ingestion_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/ingestion_selector.tsx index af8ffe36172f2f..6bf75fca0546bd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/ingestion_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/ingestion_selector.tsx @@ -11,16 +11,7 @@ import { generatePath } from 'react-router-dom'; import { useValues } from 'kea'; -import { - EuiButton, - EuiCard, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiSpacer, - EuiText, - IconType, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -37,17 +28,17 @@ import connectorLogo from '../../../../assets/images/search_connector.svg'; import crawlerLogo from '../../../../assets/images/search_crawler.svg'; import languageClientsLogo from '../../../../assets/images/search_language_clients.svg'; +import { IngestionCard } from '../../../enterprise_search_content/components/shared/ingestion_card/ingestion_card'; import { NEW_API_PATH, NEW_INDEX_METHOD_PATH, NEW_INDEX_SELECT_CONNECTOR_PATH, } from '../../../enterprise_search_content/routes'; import { HttpLogic } from '../../../shared/http/http_logic'; -import { KibanaLogic } from '../../../shared/kibana'; -import { EuiLinkTo } from '../../../shared/react_router_helpers'; -import { ConnectorIcon } from './icons/connector'; -import { CrawlerIcon } from './icons/crawler'; +import { ConnectorIcon } from '../../../shared/icons/connector'; +import { CrawlerIcon } from '../../../shared/icons/crawler'; +import { KibanaLogic } from '../../../shared/kibana'; export const IngestionSelector: React.FC = () => { const { @@ -214,62 +205,3 @@ export const IngestionSelector: React.FC = () => { ); }; - -interface IngestionCardProps { - buttonIcon: IconType; - buttonLabel: string; - description: string; - href?: string; - isDisabled?: boolean; - logo: IconType; - onClick?: () => void; - title: string; -} - -const IngestionCard: React.FC = ({ - buttonIcon, - buttonLabel, - description, - href, - isDisabled, - logo, - onClick, - title, -}) => { - return ( - - - - - - {title} - - - - } - description={ - - {description} - - } - footer={ - onClick ? ( - - {buttonLabel} - - ) : ( - - - {buttonLabel} - - - ) - } - /> - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/icons/connector.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/icons/connector.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/icons/connector.tsx rename to x-pack/plugins/enterprise_search/public/applications/shared/icons/connector.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/icons/crawler.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/icons/crawler.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/icons/crawler.tsx rename to x-pack/plugins/enterprise_search/public/applications/shared/icons/crawler.tsx diff --git a/x-pack/plugins/file_upload/public/importer/importer.ts b/x-pack/plugins/file_upload/public/importer/importer.ts index 14629e041082d9..ba43c3c0849a90 100644 --- a/x-pack/plugins/file_upload/public/importer/importer.ts +++ b/x-pack/plugins/file_upload/public/importer/importer.ts @@ -23,10 +23,27 @@ const REDUCED_CHUNK_SIZE = 100; export const MAX_CHUNK_CHAR_COUNT = 1000000; export const IMPORT_RETRIES = 5; const STRING_CHUNKS_MB = 100; +const DEFAULT_TIME_FIELD = '@timestamp'; export abstract class Importer implements IImporter { protected _docArray: ImportDoc[] = []; private _chunkSize = CHUNK_SIZE; + private _index: string | undefined; + private _pipeline: IngestPipeline | undefined; + private _timeFieldName: string | undefined; + private _initialized = false; + + public initialized() { + return this._initialized; + } + + public getIndex() { + return this._index; + } + + public getTimeField() { + return this._timeFieldName; + } public read(data: ArrayBuffer) { const decoder = new TextDecoder(); @@ -82,6 +99,19 @@ export abstract class Importer implements IImporter { } : {}; + this._index = index; + this._pipeline = pipeline; + + // if an @timestamp field has been added to the + // mappings, use this field as the time field. + // This relies on the field being populated by + // the ingest pipeline on ingest + this._timeFieldName = isPopulatedObject(mappings.properties, [DEFAULT_TIME_FIELD]) + ? DEFAULT_TIME_FIELD + : undefined; + + this._initialized = true; + return await callImportRoute({ id: undefined, index, @@ -180,6 +210,39 @@ export abstract class Importer implements IImporter { return result; } + + private _getFirstReadDocs(count = 1): object[] { + const firstReadDocs = this._docArray.slice(0, count); + return firstReadDocs.map((doc) => (typeof doc === 'string' ? JSON.parse(doc) : doc)); + } + + private _getLastReadDocs(count = 1): object[] { + const lastReadDocs = this._docArray.slice(-count); + return lastReadDocs.map((doc) => (typeof doc === 'string' ? JSON.parse(doc) : doc)); + } + + public async previewIndexTimeRange() { + if (this._initialized === false || this._pipeline === undefined) { + throw new Error('Import has not been initialized'); + } + + // take the first and last 10 docs from the file, to reduce the chance of getting + // bad data or out of order data. + const firstDocs = this._getFirstReadDocs(10); + const lastDocs = this._getLastReadDocs(10); + + const body = JSON.stringify({ + docs: firstDocs.concat(lastDocs), + pipeline: this._pipeline, + timeField: this._timeFieldName, + }); + return await getHttp().fetch<{ start: number | null; end: number | null }>({ + path: `/internal/file_upload/preview_index_time_range`, + method: 'POST', + version: '1', + body, + }); + } } function populateFailures( diff --git a/x-pack/plugins/file_upload/public/importer/types.ts b/x-pack/plugins/file_upload/public/importer/types.ts index 2a5efaa0d1dc92..fbd248f0455ff5 100644 --- a/x-pack/plugins/file_upload/public/importer/types.ts +++ b/x-pack/plugins/file_upload/public/importer/types.ts @@ -52,4 +52,8 @@ export interface IImporter { pipelineId: string | undefined, setImportProgress: (progress: number) => void ): Promise; + initialized(): boolean; + getIndex(): string | undefined; + getTimeField(): string | undefined; + previewIndexTimeRange(): Promise<{ start: number | null; end: number | null }>; } diff --git a/x-pack/plugins/file_upload/public/index.ts b/x-pack/plugins/file_upload/public/index.ts index 135f26c69ad2f5..b97ed0f0963d89 100644 --- a/x-pack/plugins/file_upload/public/index.ts +++ b/x-pack/plugins/file_upload/public/index.ts @@ -15,3 +15,4 @@ export type { Props as IndexNameFormProps } from './components/geo_upload_form/i export type { FileUploadPluginStart } from './plugin'; export type { FileUploadComponentProps, FileUploadGeoResults } from './lazy_load_bundle'; +export type { IImporter } from './importer/types'; diff --git a/x-pack/plugins/file_upload/server/preview_index_time_range.ts b/x-pack/plugins/file_upload/server/preview_index_time_range.ts new file mode 100644 index 00000000000000..ae1e8deaeac048 --- /dev/null +++ b/x-pack/plugins/file_upload/server/preview_index_time_range.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import dateMath from '@kbn/datemath'; +import type { + IngestPipeline, + IngestSimulateDocument, +} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { IScopedClusterClient } from '@kbn/core/server'; + +type Doc = IngestSimulateDocument['_source']; + +/** + * Returns the start and end time range in epoch milliseconds for a given set of documents + * @param client IScopedClusterClient + * @param timeField Time field name + * @param pipeline ingest pipeline config + * @param docs array of documents + * @returns start and end time range in epoch milliseconds + */ +export async function previewIndexTimeRange( + client: IScopedClusterClient, + timeField: string, + pipeline: IngestPipeline, + docs: Doc[] +): Promise<{ start: number | null; end: number | null }> { + const resp = await client.asInternalUser.ingest.simulate({ + pipeline, + docs: docs.map((doc, i) => ({ + _index: 'index', + _id: `id${i}`, + _source: doc, + })), + }); + + const timeFieldValues: string[] = resp.docs.map((doc) => doc.doc?._source[timeField]); + + const epochs: number[] = timeFieldValues + .map((timeFieldValue) => dateMath.parse(timeFieldValue)?.valueOf()) + .filter((epoch) => epoch !== undefined) as number[]; + + return { + start: Math.min(...epochs), + end: Math.max(...epochs), + }; +} diff --git a/x-pack/plugins/file_upload/server/routes.ts b/x-pack/plugins/file_upload/server/routes.ts index 707314b5bd4a23..6d80f8f05cb3a7 100644 --- a/x-pack/plugins/file_upload/server/routes.ts +++ b/x-pack/plugins/file_upload/server/routes.ts @@ -6,8 +6,8 @@ */ import { schema } from '@kbn/config-schema'; -import { IScopedClusterClient } from '@kbn/core/server'; -import { CoreSetup, Logger } from '@kbn/core/server'; +import type { IScopedClusterClient } from '@kbn/core/server'; +import type { CoreSetup, Logger } from '@kbn/core/server'; import type { IndicesIndexSettings, MappingTypeMapping, @@ -26,8 +26,9 @@ import { analyzeFileQuerySchema, runtimeMappingsSchema, } from './schemas'; -import { StartDeps } from './types'; +import type { StartDeps } from './types'; import { checkFileUploadPrivileges } from './check_privileges'; +import { previewIndexTimeRange } from './preview_index_time_range'; function importData( client: IScopedClusterClient, @@ -270,6 +271,49 @@ export function fileUploadRoutes(coreSetup: CoreSetup, logge runtimeMappings ); + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + } + ); + + /** + * @apiGroup FileDataVisualizer + * + * @api {post} /internal/file_upload/preview_index_time_range Predict the time range for an index using example documents + * @apiName PreviewIndexTimeRange + * @apiDescription Predict the time range for an index using example documents + */ + router.versioned + .post({ + path: '/internal/file_upload/preview_index_time_range', + access: 'internal', + options: { + tags: ['access:fileUpload:analyzeFile'], + }, + }) + .addVersion( + { + version: '1', + validate: { + request: { + body: schema.object({ + docs: schema.arrayOf(schema.any()), + pipeline: schema.any(), + timeField: schema.string(), + }), + }, + }, + }, + async (context, request, response) => { + try { + const { docs, pipeline, timeField } = request.body; + const esClient = (await context.core).elasticsearch.client; + const resp = await previewIndexTimeRange(esClient, timeField, pipeline, docs); + return response.ok({ body: resp, }); diff --git a/x-pack/plugins/file_upload/tsconfig.json b/x-pack/plugins/file_upload/tsconfig.json index 7bd19d74f6dc5b..8b7fa66cbf14b6 100644 --- a/x-pack/plugins/file_upload/tsconfig.json +++ b/x-pack/plugins/file_upload/tsconfig.json @@ -15,6 +15,7 @@ "@kbn/ml-is-populated-object", "@kbn/config-schema", "@kbn/code-editor", + "@kbn/datemath", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/fleet/common/services/is_agent_upgradeable.ts b/x-pack/plugins/fleet/common/services/is_agent_upgradeable.ts index 8d2108ee83ef0d..06043af3fdb4bf 100644 --- a/x-pack/plugins/fleet/common/services/is_agent_upgradeable.ts +++ b/x-pack/plugins/fleet/common/services/is_agent_upgradeable.ts @@ -29,7 +29,7 @@ export const LATEST_VERSION_NOT_VALID_ERROR = 'latest version is not valid.'; export const AGENT_ALREADY_ON_LATEST_ERROR = `agent is already running on the latest available version.`; export const AGENT_ON_GREATER_VERSION_ERROR = `agent is running on a version greater than the latest available version.`; -export function isAgentUpgradeAvailable(agent: Agent, latestAgentVersion?: string) { +export function isAgentUpgradeAvailable(agent: Agent, latestAgentVersion?: string): boolean { return ( latestAgentVersion && isAgentUpgradeable(agent) && @@ -38,7 +38,7 @@ export function isAgentUpgradeAvailable(agent: Agent, latestAgentVersion?: strin ); } -export function isAgentUpgradeable(agent: Agent) { +export function isAgentUpgradeable(agent: Agent): boolean { if (agent.unenrollment_started_at || agent.unenrolled_at) { return false; } @@ -54,7 +54,7 @@ export function isAgentUpgradeable(agent: Agent) { return true; } -export function isAgentUpgradeableToVersion(agent: Agent, versionToUpgrade?: string) { +export function isAgentUpgradeableToVersion(agent: Agent, versionToUpgrade?: string): boolean { const isAgentUpgradeableCheck = isAgentUpgradeable(agent); if (!isAgentUpgradeableCheck) return false; let agentVersion: string; @@ -69,7 +69,10 @@ export function isAgentUpgradeableToVersion(agent: Agent, versionToUpgrade?: str return isNotDowngrade(agentVersion, versionToUpgrade); } -export const isAgentVersionLessThanLatest = (agentVersion: string, latestAgentVersion: string) => { +export const isAgentVersionLessThanLatest = ( + agentVersion: string, + latestAgentVersion: string +): boolean => { // make sure versions are only the number before comparison const agentVersionNumber = semverCoerce(agentVersion); if (!agentVersionNumber) throw new Error(`${INVALID_VERSION_ERROR}`); @@ -84,7 +87,7 @@ export const getNotUpgradeableMessage = ( agent: Agent, latestAgentVersion?: string, versionToUpgrade?: string -) => { +): string | undefined => { let agentVersion: string; if (typeof agent?.local_metadata?.elastic?.agent?.version === 'string') { agentVersion = agent.local_metadata.elastic.agent.version; @@ -132,11 +135,11 @@ export const getNotUpgradeableMessage = ( return undefined; }; -const isNotDowngrade = (agentVersion: string, versionToUpgrade: string) => { +const isNotDowngrade = (agentVersion: string, versionToUpgrade: string): boolean => { const agentVersionNumber = semverCoerce(agentVersion); if (!agentVersionNumber) throw new Error(`${INVALID_VERSION_ERROR}`); const versionToUpgradeNumber = semverCoerce(versionToUpgrade); - if (!versionToUpgradeNumber) throw new Error(`${SELECTED_VERSION_ERROR}`); + if (!versionToUpgradeNumber) return true; return semverGt(versionToUpgradeNumber, agentVersionNumber); }; @@ -173,3 +176,16 @@ export function isAgentUpgrading(agent: Agent) { } return agent.upgrade_started_at && !agent.upgraded_at; } + +export const differsOnlyInPatch = ( + versionA: string, + versionB: string, + allowEqualPatch: boolean = true +): boolean => { + const [majorA, minorA, patchA] = versionA.split('.'); + const [majorB, minorB, patchB] = versionB.split('.'); + + return ( + majorA === majorB && minorA === minorB && (allowEqualPatch ? patchA >= patchB : patchA > patchB) + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx index aeea51987f43cf..57a23cbe412f80 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx @@ -45,11 +45,11 @@ import { useConfig, sendGetAgentStatus, useAgentVersion, - differsOnlyInPatch, } from '../../../../hooks'; import { sendGetAgentsAvailableVersions } from '../../../../hooks'; import { + differsOnlyInPatch, getNotUpgradeableMessage, isAgentUpgradeableToVersion, } from '../../../../../../../common/services/is_agent_upgradeable'; diff --git a/x-pack/plugins/fleet/public/hooks/use_agent_version.ts b/x-pack/plugins/fleet/public/hooks/use_agent_version.ts index 85409759cd88a0..b9ae1e985b277b 100644 --- a/x-pack/plugins/fleet/public/hooks/use_agent_version.ts +++ b/x-pack/plugins/fleet/public/hooks/use_agent_version.ts @@ -8,6 +8,8 @@ import { useEffect, useState } from 'react'; import semverRcompare from 'semver/functions/rcompare'; import semverLt from 'semver/functions/lt'; +import { differsOnlyInPatch } from '../../common/services'; + import { useKibanaVersion } from './use_kibana_version'; import { sendGetAgentsAvailableVersions } from './use_request'; @@ -50,16 +52,3 @@ export const useAgentVersion = (): string | undefined => { return agentVersion; }; - -export const differsOnlyInPatch = ( - versionA: string, - versionB: string, - allowEqualPatch: boolean = true -): boolean => { - const [majorA, minorA, patchA] = versionA.split('.'); - const [majorB, minorB, patchB] = versionB.split('.'); - - return ( - majorA === majorB && minorA === minorB && (allowEqualPatch ? patchA >= patchB : patchA > patchB) - ); -}; diff --git a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.test.ts b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.test.ts index 62f34559c79ee3..ce8f181d05cbc6 100644 --- a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.test.ts +++ b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.test.ts @@ -28,6 +28,11 @@ describe('upgrade handler', () => { expect(() => checkKibanaVersion('8.4.1-SNAPSHOT', '8.4.0', true)).not.toThrowError(); }); + it('should not throw if not force is specified and patch is newer', () => { + expect(() => checkKibanaVersion('8.4.1', '8.4.0', false)).not.toThrowError(); + expect(() => checkKibanaVersion('8.4.1-SNAPSHOT', '8.4.0', false)).not.toThrowError(); + }); + it('should throw if force is specified and minor is newer', () => { expect(() => checkKibanaVersion('8.5.0', '8.4.0', true)).toThrowError(); }); diff --git a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts index 471264fbe1fd00..867ad180f574a9 100644 --- a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts @@ -26,6 +26,7 @@ import { isAgentUpgrading, getNotUpgradeableMessage, isAgentUpgradeableToVersion, + differsOnlyInPatch, } from '../../../common/services'; import { getMaxVersion } from '../../../common/services/get_min_max_version'; import { getAgentById } from '../../services/agents'; @@ -198,7 +199,11 @@ export const checkKibanaVersion = (version: string, kibanaVersion: string, force if (!versionToUpgradeNumber) throw new AgentRequestInvalidError(`Version to upgrade ${versionToUpgradeNumber} is not valid`); - if (!force && semverGt(versionToUpgradeNumber, kibanaVersionNumber)) { + if ( + !force && + semverGt(versionToUpgradeNumber, kibanaVersionNumber) && + !differsOnlyInPatch(versionToUpgradeNumber, kibanaVersionNumber) + ) { throw new AgentRequestInvalidError( `Cannot upgrade agent to ${versionToUpgradeNumber} because it is higher than the installed kibana version ${kibanaVersionNumber}` ); diff --git a/x-pack/plugins/infra/public/components/asset_details/links/link_to_alerts_page.tsx b/x-pack/plugins/infra/public/components/asset_details/links/link_to_alerts_page.tsx index 95d0c782d49e78..d457be8ab98f19 100644 --- a/x-pack/plugins/infra/public/components/asset_details/links/link_to_alerts_page.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/links/link_to_alerts_page.tsx @@ -58,7 +58,7 @@ export const LinkToAlertsHomePage = () => { const linkToAlertsPage = http.basePath.prepend(ALERTS_PATH); return ( - + + } isOpen={isPopoverOpen} offset={10} diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/table/popover.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/common/popover.tsx similarity index 66% rename from x-pack/plugins/infra/public/pages/metrics/hosts/components/table/popover.tsx rename to x-pack/plugins/infra/public/pages/metrics/hosts/components/common/popover.tsx index 533579ab93bd0c..b7288c39c2393e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/table/popover.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/common/popover.tsx @@ -5,17 +5,15 @@ * 2.0. */ -import React, { useCallback, useRef } from 'react'; +import React, { useCallback } from 'react'; import { EuiPopover, EuiIcon } from '@elastic/eui'; -import { css } from '@emotion/react'; import { useBoolean } from '../../../../../hooks/use_boolean'; export const Popover = ({ children }: { children: React.ReactNode }) => { - const buttonRef = useRef(null); const [isPopoverOpen, { off: closePopover, toggle: togglePopover }] = useBoolean(false); const onButtonClick = useCallback( - (e: React.MouseEvent) => { + (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); togglePopover(); @@ -26,17 +24,10 @@ export const Popover = ({ children }: { children: React.ReactNode }) => { return ( (buttonRef.current = el)} button={ - + } isOpen={isPopoverOpen} closePopover={closePopover} diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/controls_title.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/controls_title.tsx index 53ccb87f17105a..1f98d5a59e8087 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/controls_title.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/controls_title.tsx @@ -6,31 +6,33 @@ */ import React from 'react'; -import { EuiFormLabel, EuiLink, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFormLabel, EuiText, EuiLink, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { availableControlsPanels } from '../../hooks/use_control_panels_url_state'; -import { Popover } from '../table/popover'; +import { Popover } from '../common/popover'; const helpMessages = { [availableControlsPanels.SERVICE_NAME]: ( - - - - ), - }} - /> + + + + + ), + }} + /> + ), }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/limit_options.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/limit_options.tsx index d1ad1f86034e71..8969ed97e2affa 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/limit_options.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/limit_options.tsx @@ -62,7 +62,9 @@ export const LimitOptions = ({ limit, onChange }: Props) => { })} anchorClassName="eui-fullWidth" > - +
diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/table/column_header.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/table/column_header.tsx index 04f5ae78ded548..e5c5858fba86e9 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/table/column_header.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/table/column_header.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiFlexGroup } from '@elastic/eui'; import { css } from '@emotion/react'; import { TooltipContent } from '../../../../../components/lens/metric_explanation/tooltip_content'; -import { Popover } from './popover'; +import { Popover } from '../common/popover'; interface Props { label: string; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metrics_grid.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metrics_grid.tsx index 37424447d9d72f..91078d59e1be9a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metrics_grid.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metrics_grid.tsx @@ -11,7 +11,7 @@ import { findInventoryModel } from '@kbn/metrics-data-access-plugin/common'; import useAsync from 'react-use/lib/useAsync'; import { HostMetricsExplanationContent } from '../../../../../../components/lens'; import { Chart } from './chart'; -import { Popover } from '../../table/popover'; +import { Popover } from '../../common/popover'; import { useMetricsDataViewContext } from '../../../hooks/use_data_view'; export const MetricsGrid = () => { diff --git a/x-pack/plugins/infra/public/pages/metrics/settings/indices_configuration_form_state.ts b/x-pack/plugins/infra/public/pages/metrics/settings/indices_configuration_form_state.ts index 55922dd05bfee4..04f7b91118d901 100644 --- a/x-pack/plugins/infra/public/pages/metrics/settings/indices_configuration_form_state.ts +++ b/x-pack/plugins/infra/public/pages/metrics/settings/indices_configuration_form_state.ts @@ -99,16 +99,34 @@ export const useIndicesConfigurationFormState = ({ const isFormValid = useMemo(() => errors.length <= 0, [errors]); - const isFormDirty = useMemo(() => Object.keys(formStateChanges).length > 0, [formStateChanges]); + const getUnsavedChanges = ({ + changedConfig, + existingConfig, + }: { + changedConfig: FormStateChanges; + existingConfig?: FormState; + }) => { + return Object.fromEntries( + Object.entries(changedConfig).filter(([key, value]) => { + const existingValue = existingConfig?.[key as keyof FormState]; + // don't highlight changes that were added and removed + if (value === '' && existingValue == null) { + return false; + } + + return existingValue !== value; + }) + ); + }; return { errors, fieldProps, formState, formStateChanges, - isFormDirty, isFormValid, resetForm, + getUnsavedChanges, }; }; diff --git a/x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_form_state.tsx b/x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_form_state.tsx index d97d66cd5c05de..d52e2c70d31f3c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_form_state.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_form_state.tsx @@ -12,19 +12,21 @@ import { useIndicesConfigurationFormState } from './indices_configuration_form_s export const useSourceConfigurationFormState = ( configuration?: MetricsSourceConfigurationProperties ) => { + const initialFormState = useMemo( + () => + configuration + ? { + name: configuration.name, + description: configuration.description, + metricAlias: configuration.metricAlias, + anomalyThreshold: configuration.anomalyThreshold, + } + : undefined, + [configuration] + ); + const indicesConfigurationFormState = useIndicesConfigurationFormState({ - initialFormState: useMemo( - () => - configuration - ? { - name: configuration.name, - description: configuration.description, - metricAlias: configuration.metricAlias, - anomalyThreshold: configuration.anomalyThreshold, - } - : undefined, - [configuration] - ), + initialFormState, }); const errors = useMemo( @@ -36,11 +38,6 @@ export const useSourceConfigurationFormState = ( indicesConfigurationFormState.resetForm(); }, [indicesConfigurationFormState]); - const isFormDirty = useMemo( - () => indicesConfigurationFormState.isFormDirty, - [indicesConfigurationFormState.isFormDirty] - ); - const isFormValid = useMemo( () => indicesConfigurationFormState.isFormValid, [indicesConfigurationFormState.isFormValid] @@ -66,13 +63,20 @@ export const useSourceConfigurationFormState = ( [indicesConfigurationFormState.formStateChanges] ); + const getUnsavedChanges = useCallback(() => { + return indicesConfigurationFormState.getUnsavedChanges({ + changedConfig: formState, + existingConfig: initialFormState, + }); + }, [formState, indicesConfigurationFormState, initialFormState]); + return { errors, formState, formStateChanges, - isFormDirty, isFormValid, indicesConfigurationProps: indicesConfigurationFormState.fieldProps, resetForm, + getUnsavedChanges, }; }; diff --git a/x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_settings.tsx b/x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_settings.tsx index a064aaf0e151fd..3da4ab36bce36c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_settings.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_settings.tsx @@ -5,18 +5,14 @@ * 2.0. */ -import { - EuiButton, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiSpacer, -} from '@elastic/eui'; +import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; import React, { useCallback } from 'react'; -import { Prompt, useEditableSettings } from '@kbn/observability-shared-plugin/public'; +import { + BottomBarActions, + Prompt, + useEditableSettings, +} from '@kbn/observability-shared-plugin/public'; import { enableInfrastructureHostsView, enableInfrastructureProfilingIntegration, @@ -59,11 +55,11 @@ export const SourceConfigurationSettings = ({ indicesConfigurationProps, errors, resetForm, - isFormDirty, isFormValid, formState, formStateChanges, - } = useSourceConfigurationFormState(source && source.configuration); + getUnsavedChanges, + } = useSourceConfigurationFormState(source?.configuration); const infraUiSettings = useEditableSettings('infra_metrics', [ enableInfrastructureHostsView, enableInfrastructureProfilingIntegration, @@ -92,7 +88,12 @@ export const SourceConfigurationSettings = ({ formState, ]); - const hasUnsavedChanges = isFormDirty || Object.keys(infraUiSettings.unsavedChanges).length > 0; + const unsavedChangesCount = Object.keys(getUnsavedChanges()).length; + const infraUiSettingsUnsavedChangesCount = Object.keys(infraUiSettings.unsavedChanges).length; + // Count changes from the feature section settings and general infra settings + const unsavedFormChangesCount = infraUiSettingsUnsavedChangesCount + unsavedChangesCount; + + const isFormDirty = infraUiSettingsUnsavedChangesCount > 0 || unsavedChangesCount > 0; const isWriteable = shouldAllowEdit && (!Boolean(source) || source?.origin !== 'internal'); @@ -171,54 +172,18 @@ export const SourceConfigurationSettings = ({ {isWriteable && ( - {isLoading || infraUiSettings.isSaving ? ( - - - - {i18n.translate('xpack.infra.sourceConfiguration.loadingButtonLabel', { - defaultMessage: 'Loading', - })} - - - - ) : ( - <> - - - - - - - - - - - - - + {isFormDirty && ( + )} )} diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 39a614b568799f..d9fd2a1c7c6e7d 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -41,6 +41,7 @@ import { } from '../data_views_service/service'; import { replaceIndexpattern } from '../state_management/lens_slice'; import { useApplicationUserMessages } from './get_application_user_messages'; +import { trackUiCounterEvents } from '../lens_ui_telemetry'; export type SaveProps = Omit & { returnToOrigin: boolean; @@ -112,6 +113,10 @@ export function App({ annotationGroups, } = useLensSelector((state) => state.lens); + const activeVisualization = visualization.activeId + ? visualizationMap[visualization.activeId] + : undefined; + const selectorDependencies = useMemo( () => ({ datasourceMap, @@ -319,6 +324,17 @@ export function App({ const runSave = useCallback( (saveProps: SaveProps, options: { saveToLibrary: boolean }) => { dispatch(applyChanges()); + const prevVisState = + persistedDoc?.visualizationType === visualization.activeId + ? persistedDoc?.state.visualization + : undefined; + const telemetryEvents = activeVisualization?.getTelemetryEventsOnSave?.( + visualization.state, + prevVisState + ); + if (telemetryEvents && telemetryEvents.length) { + trackUiCounterEvents(telemetryEvents); + } return runSaveLensVisualization( { lastKnownDoc, @@ -351,6 +367,9 @@ export function App({ ); }, [ + visualization.activeId, + visualization.state, + activeVisualization, dispatch, lastKnownDoc, getIsByValueMode, @@ -520,7 +539,7 @@ export function App({ ? datasourceMap[activeDatasourceId] : null, dispatch, - visualization: visualization.activeId ? visualizationMap[visualization.activeId] : undefined, + visualization: activeVisualization, visualizationType: visualization.activeId, visualizationState: visualization, }); diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx index b17c1313a8df0f..106eeee0377040 100644 --- a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx +++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx @@ -43,6 +43,7 @@ import { FlyoutWrapper } from './flyout_wrapper'; import { getSuggestions } from './helpers'; import { SuggestionPanel } from '../../../editor_frame_service/editor_frame/suggestion_panel'; import { useApplicationUserMessages } from '../../get_application_user_messages'; +import { trackUiCounterEvents } from '../../../lens_ui_telemetry'; export function LensEditConfigurationFlyout({ attributes, @@ -230,9 +231,24 @@ export function LensEditConfigurationFlyout({ saveByRef?.(attrs); updateByRefInput?.(savedObjectId); } + + // check if visualization type changed, if it did, don't pass the previous visualization state + const prevVisState = + previousAttributes.current.visualizationType === visualization.activeId + ? previousAttributes.current.state.visualization + : undefined; + const telemetryEvents = activeVisualization.getTelemetryEventsOnSave?.( + visualization.state, + prevVisState + ); + if (telemetryEvents && telemetryEvents.length) { + trackUiCounterEvents(telemetryEvents); + } + onApplyCb?.(); closeFlyout?.(); }, [ + visualization.activeId, savedObjectId, closeFlyout, onApplyCb, diff --git a/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.test.ts b/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.test.ts new file mode 100644 index 00000000000000..d6a6701aa658a3 --- /dev/null +++ b/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.test.ts @@ -0,0 +1,299 @@ +/* + * 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 { getColorMappingTelemetryEvents } from './color_telemetry_helpers'; +import { + ColorMapping, + EUIAmsterdamColorBlindPalette, + ElasticBrandPalette, + NeutralPalette, +} from '@kbn/coloring'; +import faker from 'faker'; +import { DEFAULT_NEUTRAL_PALETTE_INDEX } from '@kbn/coloring/src/shared_components/color_mapping/config/default_color_mapping'; + +export const DEFAULT_COLOR_MAPPING_CONFIG: ColorMapping.Config = { + assignmentMode: 'auto', + assignments: [], + specialAssignments: [ + { + rule: { + type: 'other', + }, + color: { + type: 'categorical', + paletteId: NeutralPalette.id, + colorIndex: DEFAULT_NEUTRAL_PALETTE_INDEX, + }, + touched: false, + }, + ], + paletteId: EUIAmsterdamColorBlindPalette.id, + colorMode: { + type: 'categorical', + }, +}; + +const exampleAssignment = (valuesCount = 1, type = 'categorical', overrides = {}) => { + const color = + type === 'categorical' + ? { + type: 'categorical', + paletteId: ElasticBrandPalette.id, + colorIndex: 0, + } + : { + type: 'colorCode', + colorCode: faker.internet.color(), + }; + + return { + rule: { + type: 'matchExactly', + values: Array.from({ length: valuesCount }, () => faker.random.alpha()), + }, + color, + touched: false, + ...overrides, + } as ColorMapping.Config['assignments'][0]; +}; + +const MANUAL_COLOR_MAPPING_CONFIG: ColorMapping.Config = { + assignmentMode: 'manual', + assignments: [ + exampleAssignment(4), + exampleAssignment(), + exampleAssignment(4, 'custom'), + exampleAssignment(1, 'custom'), + ], + specialAssignments: [ + { + rule: { + type: 'other', + }, + color: { + type: 'categorical', + paletteId: ElasticBrandPalette.id, + colorIndex: 2, + }, + touched: true, + }, + ], + paletteId: ElasticBrandPalette.id, + colorMode: { + type: 'categorical', + }, +}; + +const specialAssignmentsPalette: ColorMapping.Config['specialAssignments'] = [ + { + ...DEFAULT_COLOR_MAPPING_CONFIG.specialAssignments[0], + color: { + type: 'categorical', + paletteId: EUIAmsterdamColorBlindPalette.id, + colorIndex: 0, + }, + }, +]; +const specialAssignmentsCustom1: ColorMapping.Config['specialAssignments'] = [ + { + ...DEFAULT_COLOR_MAPPING_CONFIG.specialAssignments[0], + color: { + type: 'colorCode', + colorCode: '#501a0e', + }, + }, +]; +const specialAssignmentsCustom2: ColorMapping.Config['specialAssignments'] = [ + { + ...DEFAULT_COLOR_MAPPING_CONFIG.specialAssignments[0], + color: { + type: 'colorCode', + colorCode: 'red', + }, + }, +]; + +describe('color_telemetry_helpers', () => { + it('no events if color mapping is not defined', () => { + expect(getColorMappingTelemetryEvents(undefined)).toEqual([]); + }); + it('no events if no changes made in color mapping', () => { + expect( + getColorMappingTelemetryEvents(DEFAULT_COLOR_MAPPING_CONFIG, DEFAULT_COLOR_MAPPING_CONFIG) + ).toEqual([]); + expect( + getColorMappingTelemetryEvents(MANUAL_COLOR_MAPPING_CONFIG, MANUAL_COLOR_MAPPING_CONFIG) + ).toEqual([]); + }); + it('settings (default): auto color mapping, unassigned terms neutral, default palette returns correct events', () => { + expect(getColorMappingTelemetryEvents(DEFAULT_COLOR_MAPPING_CONFIG)).toEqual([ + 'lens_color_mapping_auto', + 'lens_color_mapping_palette_eui_amsterdam_color_blind', + 'lens_color_mapping_unassigned_terms_neutral', + ]); + }); + it('gradient event when user changed colorMode to gradient', () => { + expect( + getColorMappingTelemetryEvents( + { + ...DEFAULT_COLOR_MAPPING_CONFIG, + colorMode: { + type: 'gradient', + steps: [ + { + type: 'categorical', + paletteId: EUIAmsterdamColorBlindPalette.id, + colorIndex: 0, + touched: false, + }, + ], + sort: 'desc', + }, + }, + DEFAULT_COLOR_MAPPING_CONFIG + ) + ).toEqual(['lens_color_mapping_gradient']); + }); + it('settings: manual mode, custom palette, unassigned terms from palette, 2 colors with 5 terms in total', () => { + expect(getColorMappingTelemetryEvents(MANUAL_COLOR_MAPPING_CONFIG)).toEqual([ + 'lens_color_mapping_manual', + 'lens_color_mapping_palette_elastic_brand_2023', + 'lens_color_mapping_unassigned_terms_palette', + 'lens_color_mapping_colors_2_to_4', + 'lens_color_mapping_custom_colors_2', + 'lens_color_mapping_avg_count_terms_per_color_2_to_4', + ]); + expect( + getColorMappingTelemetryEvents(MANUAL_COLOR_MAPPING_CONFIG, DEFAULT_COLOR_MAPPING_CONFIG) + ).toEqual([ + 'lens_color_mapping_manual', + 'lens_color_mapping_palette_elastic_brand_2023', + 'lens_color_mapping_unassigned_terms_palette', + 'lens_color_mapping_colors_2_to_4', + 'lens_color_mapping_custom_colors_2', + 'lens_color_mapping_avg_count_terms_per_color_2_to_4', + ]); + }); + it('color, custom color and count of terms changed (even if the same event would be returned)', () => { + const config = { ...MANUAL_COLOR_MAPPING_CONFIG }; + config.assignments = config.assignments.slice(0, 3); + expect(getColorMappingTelemetryEvents(config, MANUAL_COLOR_MAPPING_CONFIG)).toEqual([ + 'lens_color_mapping_colors_2_to_4', + 'lens_color_mapping_custom_colors_1', + 'lens_color_mapping_avg_count_terms_per_color_2_to_4', + ]); + }); + + describe('color ranges', () => { + it('0 colors', () => { + const config = { ...MANUAL_COLOR_MAPPING_CONFIG }; + config.assignments = []; + expect(getColorMappingTelemetryEvents(config, MANUAL_COLOR_MAPPING_CONFIG)).toEqual([]); + }); + it('1 color', () => { + const config = { ...MANUAL_COLOR_MAPPING_CONFIG }; + + config.assignments = [exampleAssignment(4, 'custom')]; + expect(getColorMappingTelemetryEvents(config, MANUAL_COLOR_MAPPING_CONFIG)).toEqual([ + 'lens_color_mapping_colors_up_to_2', + 'lens_color_mapping_custom_colors_1', + 'lens_color_mapping_avg_count_terms_per_color_2_to_4', + ]); + }); + it('2 colors', () => { + const config = { ...MANUAL_COLOR_MAPPING_CONFIG }; + + config.assignments = [exampleAssignment(1), exampleAssignment(1)]; + expect(getColorMappingTelemetryEvents(config, MANUAL_COLOR_MAPPING_CONFIG)).toEqual([ + 'lens_color_mapping_colors_2', + 'lens_color_mapping_avg_count_terms_per_color_1', + ]); + }); + it('3 colors, 10 terms per assignment', () => { + const config = { ...MANUAL_COLOR_MAPPING_CONFIG }; + + config.assignments = Array.from({ length: 3 }, () => exampleAssignment(10)); + expect(getColorMappingTelemetryEvents(config, MANUAL_COLOR_MAPPING_CONFIG)).toEqual([ + 'lens_color_mapping_colors_2_to_4', + 'lens_color_mapping_avg_count_terms_per_color_above_4', + ]); + }); + it('7 colors, 2 terms per assignment, all custom', () => { + const config = { ...MANUAL_COLOR_MAPPING_CONFIG }; + + config.assignments = Array.from({ length: 7 }, () => exampleAssignment(2, 'custom')); + expect(getColorMappingTelemetryEvents(config, MANUAL_COLOR_MAPPING_CONFIG)).toEqual([ + 'lens_color_mapping_colors_4_to_8', + 'lens_color_mapping_custom_colors_4_to_8', + 'lens_color_mapping_avg_count_terms_per_color_2', + ]); + }); + it('12 colors', () => { + const config = { ...MANUAL_COLOR_MAPPING_CONFIG }; + + config.assignments = Array.from({ length: 12 }, () => exampleAssignment(3, 'custom')); + expect(getColorMappingTelemetryEvents(config, MANUAL_COLOR_MAPPING_CONFIG)).toEqual([ + 'lens_color_mapping_colors_8_to_16', + 'lens_color_mapping_custom_colors_8_to_16', + 'lens_color_mapping_avg_count_terms_per_color_2_to_4', + ]); + }); + it('27 colors', () => { + const config = { ...MANUAL_COLOR_MAPPING_CONFIG }; + config.assignments = Array.from({ length: 27 }, () => exampleAssignment(3, 'custom')); + expect(getColorMappingTelemetryEvents(config, MANUAL_COLOR_MAPPING_CONFIG)).toEqual([ + 'lens_color_mapping_colors_above_16', + 'lens_color_mapping_custom_colors_above_16', + 'lens_color_mapping_avg_count_terms_per_color_2_to_4', + ]); + }); + }); + + describe('unassigned terms', () => { + it('unassigned terms changed from neutral to palette', () => { + expect( + getColorMappingTelemetryEvents( + { + ...DEFAULT_COLOR_MAPPING_CONFIG, + specialAssignments: specialAssignmentsPalette, + }, + DEFAULT_COLOR_MAPPING_CONFIG + ) + ).toEqual(['lens_color_mapping_unassigned_terms_palette']); + }); + it('unassigned terms changed from palette to neutral', () => { + expect( + getColorMappingTelemetryEvents(DEFAULT_COLOR_MAPPING_CONFIG, { + ...DEFAULT_COLOR_MAPPING_CONFIG, + specialAssignments: specialAssignmentsPalette, + }) + ).toEqual(['lens_color_mapping_unassigned_terms_neutral']); + }); + it('unassigned terms changed from neutral to another custom color', () => { + expect( + getColorMappingTelemetryEvents( + { + ...DEFAULT_COLOR_MAPPING_CONFIG, + specialAssignments: specialAssignmentsCustom1, + }, + DEFAULT_COLOR_MAPPING_CONFIG + ) + ).toEqual(['lens_color_mapping_unassigned_terms_custom']); + }); + it('unassigned terms changed from custom color to another custom color', () => { + expect( + getColorMappingTelemetryEvents( + { ...DEFAULT_COLOR_MAPPING_CONFIG, specialAssignments: specialAssignmentsCustom1 }, + { + ...DEFAULT_COLOR_MAPPING_CONFIG, + specialAssignments: specialAssignmentsCustom2, + } + ) + ).toEqual(['lens_color_mapping_unassigned_terms_custom']); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.ts b/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.ts new file mode 100644 index 00000000000000..d6b7acab55c7fa --- /dev/null +++ b/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.ts @@ -0,0 +1,119 @@ +/* + * 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 { ColorMapping, NeutralPalette } from '@kbn/coloring'; +import type { + CategoricalColor, + ColorCode, + GradientColor, +} from '@kbn/coloring/src/shared_components/color_mapping/config/types'; +import { isEqual } from 'lodash'; +import { nonNullable } from '../utils'; + +const COLOR_MAPPING_PREFIX = 'lens_color_mapping_'; + +export const getColorMappingTelemetryEvents = ( + colorMapping: ColorMapping.Config | undefined, + prevColorMapping?: ColorMapping.Config +) => { + if (!colorMapping || isEqual(colorMapping, prevColorMapping)) { + return []; + } + + const { assignments, specialAssignments, assignmentMode, colorMode, paletteId } = colorMapping; + const { + assignmentMode: prevAssignmentMode, + assignments: prevAssignments, + specialAssignments: prevSpecialAssignments, + colorMode: prevColorMode, + paletteId: prevPaletteId, + } = prevColorMapping || {}; + + const assignmentModeData = assignmentMode !== prevAssignmentMode ? assignmentMode : undefined; + + const paletteData = prevPaletteId !== paletteId ? `palette_${paletteId}` : undefined; + + const gradientData = + colorMode.type === 'gradient' && prevColorMode?.type !== 'gradient' ? `gradient` : undefined; + + const unassignedTermsType = getUnassignedTermsType(specialAssignments, prevSpecialAssignments); + + const diffData = [assignmentModeData, gradientData, paletteData, unassignedTermsType].filter( + nonNullable + ); + + if (assignmentMode === 'manual') { + const colorCount = + assignments.length && !isEqual(assignments, prevAssignments) + ? `colors_${getRangeText(assignments.length)}` + : undefined; + + const prevCustomColors = prevAssignments?.filter((a) => isCustomColor(a.color)); + const customColors = assignments.filter((a) => isCustomColor(a.color)); + const customColorEvent = + customColors.length && !isEqual(prevCustomColors, customColors) + ? `custom_colors_${getRangeText(customColors.length, 1)}` + : undefined; + + const avgTermsPerColor = getAvgCountTermsPerColor(assignments, prevAssignments); + + diffData.push(...[colorCount, customColorEvent, avgTermsPerColor].filter(nonNullable)); + } + return diffData.map(constructName); +}; + +const constructName = (eventName: string) => `${COLOR_MAPPING_PREFIX}${eventName}`; + +const isCustomColor = (color: CategoricalColor | ColorCode | GradientColor): color is ColorCode => { + return color.type === 'colorCode'; +}; + +function getRangeText(n: number, min = 2, max = 16) { + if (n >= min && (n === 1 || n === 2)) { + return String(n); + } + if (n <= min) { + return `up_to_${min}`; + } else if (n > max) { + return `above_${max}`; + } + const upperBound = Math.pow(2, Math.ceil(Math.log2(n))); + const lowerBound = upperBound / 2; + return `${lowerBound}_to_${upperBound}`; +} + +const getUnassignedTermsType = ( + specialAssignments: ColorMapping.Config['specialAssignments'], + prevSpecialAssignments?: ColorMapping.Config['specialAssignments'] +) => { + return !isEqual(prevSpecialAssignments, specialAssignments) + ? `unassigned_terms_${ + isCustomColor(specialAssignments?.[0].color) + ? 'custom' + : specialAssignments?.[0].color.paletteId === NeutralPalette.id + ? NeutralPalette.id + : 'palette' + }` + : undefined; +}; + +const getTotalTermsCount = (assignments: ColorMapping.Config['assignments']) => + assignments.reduce( + (acc, cur) => ('values' in cur.rule ? acc + cur.rule.values.length : acc + 1), + 0 + ); + +const getAvgCountTermsPerColor = ( + assignments: ColorMapping.Config['assignments'], + prevAssignments?: ColorMapping.Config['assignments'] +) => { + const prevTermsCount = prevAssignments && getTotalTermsCount(prevAssignments); + const termsCount = assignments && getTotalTermsCount(assignments); + return termsCount && prevTermsCount !== termsCount + ? `avg_count_terms_per_color_${getRangeText(termsCount / assignments.length, 1, 4)}` + : undefined; +}; diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 38054103183a59..cbfba6c550ae7c 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -1320,6 +1320,10 @@ export interface Visualization { height: number; width: number }; + /** + * returns array of telemetry events for the visualization on save + */ + getTelemetryEventsOnSave?: (state: T, prevState?: T) => string[]; } // Use same technique as TriggerContext diff --git a/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx b/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx index 267c014ae1a6ca..c230f0af9d284c 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx @@ -52,6 +52,7 @@ import { LayerSettings } from './layer_settings'; import { checkTableForContainsSmallValues } from './render_helpers'; import { DatasourcePublicAPI } from '../..'; import { nonNullable, getColorMappingDefaults } from '../../utils'; +import { getColorMappingTelemetryEvents } from '../../lens_ui_telemetry/color_telemetry_helpers'; const metricLabel = i18n.translate('xpack.lens.pie.groupMetricLabelSingular', { defaultMessage: 'Metric', @@ -773,4 +774,11 @@ export const getPieVisualization = ({ ], }; }, + + getTelemetryEventsOnSave(state, prevState) { + return getColorMappingTelemetryEvents( + state?.layers[0]?.colorMapping, + prevState?.layers[0]?.colorMapping + ); + }, }); diff --git a/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_visualization.tsx b/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_visualization.tsx index bb69dfda2b0a0d..cf979910a29091 100644 --- a/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_visualization.tsx @@ -27,6 +27,7 @@ import { getSuggestions } from './suggestions'; import { TagcloudToolbar } from './tagcloud_toolbar'; import { TagsDimensionEditor } from './tags_dimension_editor'; import { DEFAULT_STATE, TAGCLOUD_LABEL } from './constants'; +import { getColorMappingTelemetryEvents } from '../../lens_ui_telemetry/color_telemetry_helpers'; const TAG_GROUP_ID = 'tags'; const METRIC_GROUP_ID = 'metric'; @@ -313,4 +314,7 @@ export const getTagcloudVisualization = ({ ToolbarComponent(props) { return ; }, + getTelemetryEventsOnSave(state, prevState) { + return getColorMappingTelemetryEvents(state?.colorMapping, prevState?.colorMapping); + }, }); diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx index d3bb805a0a3818..f2cf292df233d4 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx @@ -115,6 +115,7 @@ import { createAnnotationActions } from './annotations/actions'; import { AddLayerButton } from './add_layer'; import { LayerSettings } from './layer_settings'; import { IgnoredGlobalFiltersEntries } from '../../shared_components/ignore_global_filter'; +import { getColorMappingTelemetryEvents } from '../../lens_ui_telemetry/color_telemetry_helpers'; const XY_ID = 'lnsXY'; @@ -967,6 +968,15 @@ export const getXyVisualization = ({ getVisualizationInfo(state, frame) { return getVisualizationInfo(state, frame, paletteService, fieldFormats); }, + + getTelemetryEventsOnSave(state, prevState) { + const dataLayers = getDataLayers(state.layers); + const prevLayers = prevState ? getDataLayers(prevState.layers) : undefined; + return dataLayers.flatMap((l) => { + const prevLayer = prevLayers?.find((prevL) => prevL.layerId === l.layerId); + return getColorMappingTelemetryEvents(l.colorMapping, prevLayer?.colorMapping); + }); + }, }); const getMappedAccessors = ({ diff --git a/x-pack/plugins/apm/public/components/app/settings/bottom_bar_actions/index.tsx b/x-pack/plugins/observability_shared/public/components/bottom_bar_actions/bottom_bar_actions.tsx similarity index 58% rename from x-pack/plugins/apm/public/components/app/settings/bottom_bar_actions/index.tsx rename to x-pack/plugins/observability_shared/public/components/bottom_bar_actions/bottom_bar_actions.tsx index 1bdeec63b58ee4..3eb3cc15262d5e 100644 --- a/x-pack/plugins/apm/public/components/app/settings/bottom_bar_actions/index.tsx +++ b/x-pack/plugins/observability_shared/public/components/bottom_bar_actions/bottom_bar_actions.tsx @@ -12,6 +12,7 @@ import { EuiFlexItem, EuiHealth, EuiText, + EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; @@ -22,15 +23,19 @@ interface Props { onDiscardChanges: () => void; onSave: () => void; saveLabel: string; + appTestSubj: string; + areChangesInvalid?: boolean; } -export function BottomBarActions({ +export const BottomBarActions = ({ isLoading, onDiscardChanges, onSave, unsavedChangesCount, saveLabel, -}: Props) { + appTestSubj, + areChangesInvalid = false, +}: Props) => { return ( @@ -43,7 +48,7 @@ export function BottomBarActions({ > - {i18n.translate('xpack.apm.bottomBarActions.unsavedChanges', { + {i18n.translate('xpack.observabilityShared.bottomBarActions.unsavedChanges', { defaultMessage: '{unsavedChangesCount, plural, =0{0 unsaved changes} one {1 unsaved change} other {# unsaved changes}} ', values: { unsavedChangesCount }, @@ -54,33 +59,40 @@ export function BottomBarActions({ - {i18n.translate( - 'xpack.apm.bottomBarActions.discardChangesButton', - { - defaultMessage: 'Discard changes', - } - )} + {i18n.translate('xpack.observabilityShared.bottomBarActions.discardChangesButton', { + defaultMessage: 'Discard changes', + })} - - {saveLabel} - + + {saveLabel} + + ); -} +}; diff --git a/x-pack/plugins/observability_shared/public/index.ts b/x-pack/plugins/observability_shared/public/index.ts index 54d99163379d3e..564cb210ea2718 100644 --- a/x-pack/plugins/observability_shared/public/index.ts +++ b/x-pack/plugins/observability_shared/public/index.ts @@ -93,3 +93,4 @@ export { export { ProfilingEmptyState } from './components/profiling/profiling_empty_state'; export { FeatureFeedbackButton } from './components/feature_feedback_button/feature_feedback_button'; +export { BottomBarActions } from './components/bottom_bar_actions/bottom_bar_actions'; diff --git a/x-pack/plugins/osquery/cypress/e2e/all/live_query_run.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/live_query_run.cy.ts index 035c228623fdae..c74b253ae9d41c 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/live_query_run.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/live_query_run.cy.ts @@ -10,11 +10,13 @@ import { navigateTo } from '../../tasks/navigation'; import { checkActionItemsInResults, checkResults, + fillInQueryTimeout, inputQuery, selectAllAgents, submitQuery, typeInECSFieldInput, typeInOsqueryFieldInput, + verifyQueryTimeout, } from '../../tasks/live_query'; import { LIVE_QUERY_EDITOR, RESULTS_TABLE, RESULTS_TABLE_BUTTON } from '../../screens/live_query'; import { getAdvancedButton } from '../../screens/integrations'; @@ -93,6 +95,8 @@ describe('ALL - Live Query run custom and saved', { tags: ['@ess', '@serverless' selectAllAgents(); cy.getBySel(SAVED_QUERY_DROPDOWN_SELECT).type(`${savedQueryName}{downArrow}{enter}`); inputQuery('{selectall}{backspace}select * from users;'); + getAdvancedButton().click(); + fillInQueryTimeout('601'); cy.wait(1000); submitQuery(); checkResults(); @@ -100,6 +104,7 @@ describe('ALL - Live Query run custom and saved', { tags: ['@ess', '@serverless' cy.get('[aria-label="Run query"]').first().should('be.visible').click(); cy.getBySel(LIVE_QUERY_EDITOR).contains('select * from users;'); + verifyQueryTimeout('601'); }); it('should open query details by clicking the details icon', () => { diff --git a/x-pack/plugins/osquery/cypress/tasks/live_query.ts b/x-pack/plugins/osquery/cypress/tasks/live_query.ts index cc9319633befcd..b1ad8124dc8c6a 100644 --- a/x-pack/plugins/osquery/cypress/tasks/live_query.ts +++ b/x-pack/plugins/osquery/cypress/tasks/live_query.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { getAdvancedButton } from '../screens/integrations'; import { LIVE_QUERY_EDITOR, OSQUERY_FLYOUT_BODY_EDITOR } from '../screens/live_query'; import { ServerlessRoleName } from '../support/roles'; import { waitForAlertsToPopulate } from '../../../../test/security_solution_cypress/cypress/tasks/create_new_rule'; @@ -46,6 +47,13 @@ export const fillInQueryTimeout = (timeout: string) => { }); }; +export const verifyQueryTimeout = (timeout: string) => { + getAdvancedButton().click(); + cy.getBySel('advanced-accordion-content').within(() => { + cy.getBySel('timeout-input').should('have.value', timeout); + }); +}; + // sometimes the results get stuck in the tests, this is a workaround export const checkResults = () => { cy.getBySel('osqueryResultsTable').then(($table) => { diff --git a/x-pack/plugins/osquery/cypress/tasks/saved_queries.ts b/x-pack/plugins/osquery/cypress/tasks/saved_queries.ts index 4b0e40e577674a..0ff06beed1d796 100644 --- a/x-pack/plugins/osquery/cypress/tasks/saved_queries.ts +++ b/x-pack/plugins/osquery/cypress/tasks/saved_queries.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { getAdvancedButton } from '../screens/integrations'; import { RESULTS_TABLE_BUTTON, RESULTS_TABLE_COLUMNS_BUTTON } from '../screens/live_query'; import { closeToastIfVisible, generateRandomStringName } from './integrations'; import { @@ -14,11 +15,14 @@ import { inputQuery, selectAllAgents, submitQuery, + fillInQueryTimeout, + verifyQueryTimeout, } from './live_query'; import { navigateTo } from './navigation'; export const getSavedQueriesComplexTest = () => describe('Saved queries Complex Test', () => { + const timeout = '601'; const suffix = generateRandomStringName(1)[0]; const savedQueryId = `Saved-Query-Id-${suffix}`; const savedQueryDescription = `Test saved query description ${suffix}`; @@ -32,6 +36,8 @@ export const getSavedQueriesComplexTest = () => cy.contains('New live query').click(); selectAllAgents(); inputQuery(BIG_QUERY); + getAdvancedButton().click(); + fillInQueryTimeout(timeout); submitQuery(); checkResults(); // enter fullscreen @@ -92,6 +98,7 @@ export const getSavedQueriesComplexTest = () => cy.contains(savedQueryId); cy.get(`[aria-label="Run ${savedQueryId}"]`).click(); selectAllAgents(); + verifyQueryTimeout(timeout); submitQuery(); // edit saved query @@ -104,6 +111,7 @@ export const getSavedQueriesComplexTest = () => // Run in test configuration cy.contains('Test configuration').click(); selectAllAgents(); + verifyQueryTimeout(timeout); submitQuery(); checkResults(); diff --git a/x-pack/plugins/osquery/public/actions/actions_table.tsx b/x-pack/plugins/osquery/public/actions/actions_table.tsx index cd5e1a685d33bb..a6c66855f12cc6 100644 --- a/x-pack/plugins/osquery/public/actions/actions_table.tsx +++ b/x-pack/plugins/osquery/public/actions/actions_table.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { isArray, isEmpty, pickBy, map } from 'lodash'; +import { isArray, isEmpty, pickBy, map, isNumber } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiBasicTable, @@ -20,6 +20,7 @@ import { import React, { useState, useCallback, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; +import { QUERY_TIMEOUT } from '../../common/constants'; import { removeMultilines } from '../../common/utils/build_query/remove_multilines'; import { useAllLiveQueries } from './use_all_live_queries'; import type { SearchHit } from '../../common/search_strategy'; @@ -136,6 +137,7 @@ const ActionsTableComponent = () => { query: item._source.queries[0].query, ecs_mapping: item._source.queries[0].ecs_mapping, savedQueryId: item._source.queries[0].saved_query_id, + timeout: item._source.queries[0].timeout ?? QUERY_TIMEOUT.DEFAULT, agentSelection: { agents: item._source.agent_ids, allAgentsSelected: item._source.agent_all, @@ -143,7 +145,7 @@ const ActionsTableComponent = () => { policiesSelected: item._source.agent_policy_ids, }, }, - (value) => !isEmpty(value) + (value) => !isEmpty(value) || isNumber(value) ), }); }, diff --git a/x-pack/plugins/osquery/public/agents/use_all_agents.ts b/x-pack/plugins/osquery/public/agents/use_all_agents.ts index 12b98bccc0ed57..7d09470397e762 100644 --- a/x-pack/plugins/osquery/public/agents/use_all_agents.ts +++ b/x-pack/plugins/osquery/public/agents/use_all_agents.ts @@ -42,7 +42,7 @@ export const useAllAgents = (searchValue = '', opts: RequestOptions = { perPage: kuery = `(${osqueryPolicies.map((p) => `policy_id:${p}`).join(' or ')})`; if (searchValue) { - kuery += ` and (local_metadata.host.hostname:*${searchValue}* or local_metadata.elastic.agent.id:*${searchValue}*)`; + kuery += ` and (local_metadata.host.hostname.keyword:*${searchValue}* or local_metadata.elastic.agent.id:*${searchValue}*)`; } else { kuery += ` and (status:online ${ agentIds?.length ? `or local_metadata.elastic.agent.id:(${agentIds.join(' or ')})` : '' diff --git a/x-pack/plugins/osquery/public/packs/queries/ecs_field_css.ts b/x-pack/plugins/osquery/public/packs/queries/ecs_field_css.ts index 7dc3feeb62c3ee..444ff6a4f05cfd 100644 --- a/x-pack/plugins/osquery/public/packs/queries/ecs_field_css.ts +++ b/x-pack/plugins/osquery/public/packs/queries/ecs_field_css.ts @@ -38,6 +38,7 @@ export const fieldIconCss = { export const fieldSpanCss = { paddingTop: '0 !important', paddingBottom: '0 !important', + paddingLeft: '5px', }; export const descriptionWrapperCss = { diff --git a/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx b/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx index 51f046768aface..4bb0877d5ca36c 100644 --- a/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx +++ b/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx @@ -162,7 +162,7 @@ const ECSComboboxFieldComponent: React.FC = ({
- {option.value.field} + {option.value.field} @@ -394,7 +394,7 @@ const OsqueryColumnFieldComponent: React.FC = ({ > - {option.value.suggestion_label} + {option.value.suggestion_label} diff --git a/x-pack/plugins/osquery/public/saved_queries/form/playground_flyout.tsx b/x-pack/plugins/osquery/public/saved_queries/form/playground_flyout.tsx index 518f03be92f353..3d7dbf1b3a3dd0 100644 --- a/x-pack/plugins/osquery/public/saved_queries/form/playground_flyout.tsx +++ b/x-pack/plugins/osquery/public/saved_queries/form/playground_flyout.tsx @@ -10,6 +10,7 @@ import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { useFormContext } from 'react-hook-form'; +import { QUERY_TIMEOUT } from '../../../common/constants'; import { LiveQuery } from '../../live_queries'; const euiFlyoutHeaderCss = { @@ -28,7 +29,7 @@ const PlaygroundFlyoutComponent: React.FC = ({ enabled, o // @ts-expect-error update types const { serializer, watch } = useFormContext(); const watchedValues = watch(); - const { query, ecs_mapping: ecsMapping, id } = watchedValues; + const { query, ecs_mapping: ecsMapping, id, timeout } = watchedValues; /* recalculate the form data when ecs_mapping changes */ // eslint-disable-next-line react-hooks/exhaustive-deps const serializedFormData = useMemo(() => serializer(watchedValues), [ecsMapping]); @@ -52,6 +53,7 @@ const PlaygroundFlyoutComponent: React.FC = ({ enabled, o query={query} ecs_mapping={serializedFormData.ecs_mapping} savedQueryId={id} + timeout={timeout || QUERY_TIMEOUT.DEFAULT} queryField={false} ecsMappingField={false} /> diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index 732097aedbc2b4..fe8ec46b752c57 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -1947,6 +1947,11 @@ const mockTimelineModelColumns: TimelineModel['columns'] = [ id: 'user.name', initialWidth: 180, }, + { + columnHeaderType: 'not-filtered', + id: 'kibana.alert.workflow_assignee_ids', + initialWidth: 180, + }, ]; export const mockTimelineModel: TimelineModel = { activeTab: TimelineTabs.query, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx index b4b0a07fd7ab29..fc5d91bcaf8e5b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx @@ -6,16 +6,78 @@ */ import type { ExistsFilter, Filter } from '@kbn/es-query'; +import { tableDefaults } from '@kbn/securitysolution-data-table'; +import { createLicenseServiceMock } from '../../../../common/license/mocks'; import { buildAlertAssigneesFilter, buildAlertsFilter, buildAlertStatusesFilter, buildAlertStatusFilter, buildThreatMatchFilter, + getAlertsDefaultModel, + getAlertsPreviewDefaultModel, } from './default_config'; jest.mock('./actions'); +const basicBaseColumns = [ + { + columnHeaderType: 'not-filtered', + displayAsText: 'Severity', + id: 'kibana.alert.severity', + initialWidth: 105, + }, + { + columnHeaderType: 'not-filtered', + displayAsText: 'Risk Score', + id: 'kibana.alert.risk_score', + initialWidth: 100, + }, + { + columnHeaderType: 'not-filtered', + displayAsText: 'Reason', + id: 'kibana.alert.reason', + initialWidth: 450, + }, + { columnHeaderType: 'not-filtered', id: 'host.name' }, + { columnHeaderType: 'not-filtered', id: 'user.name' }, + { columnHeaderType: 'not-filtered', id: 'process.name' }, + { columnHeaderType: 'not-filtered', id: 'file.name' }, + { columnHeaderType: 'not-filtered', id: 'source.ip' }, + { columnHeaderType: 'not-filtered', id: 'destination.ip' }, +]; + +const platinumBaseColumns = [ + { + columnHeaderType: 'not-filtered', + displayAsText: 'Severity', + id: 'kibana.alert.severity', + initialWidth: 105, + }, + { + columnHeaderType: 'not-filtered', + displayAsText: 'Risk Score', + id: 'kibana.alert.risk_score', + initialWidth: 100, + }, + { + columnHeaderType: 'not-filtered', + displayAsText: 'Reason', + id: 'kibana.alert.reason', + initialWidth: 450, + }, + { columnHeaderType: 'not-filtered', id: 'host.name' }, + { columnHeaderType: 'not-filtered', id: 'host.risk.calculated_level' }, + { columnHeaderType: 'not-filtered', id: 'user.name' }, + { columnHeaderType: 'not-filtered', id: 'user.risk.calculated_level' }, + { columnHeaderType: 'not-filtered', id: 'kibana.alert.host.criticality_level' }, + { columnHeaderType: 'not-filtered', id: 'kibana.alert.user.criticality_level' }, + { columnHeaderType: 'not-filtered', id: 'process.name' }, + { columnHeaderType: 'not-filtered', id: 'file.name' }, + { columnHeaderType: 'not-filtered', id: 'source.ip' }, + { columnHeaderType: 'not-filtered', id: 'destination.ip' }, +]; + describe('alerts default_config', () => { describe('buildAlertsRuleIdFilter', () => { test('given a rule id this will return an array with a single filter', () => { @@ -200,6 +262,122 @@ describe('alerts default_config', () => { }); }); + describe('getAlertsDefaultModel', () => { + test('returns correct model for Basic license', () => { + const licenseServiceMock = createLicenseServiceMock(); + licenseServiceMock.isPlatinumPlus.mockReturnValue(false); + const model = getAlertsDefaultModel(licenseServiceMock); + + const expected = { + ...tableDefaults, + showCheckboxes: true, + columns: [ + { columnHeaderType: 'not-filtered', id: '@timestamp', initialWidth: 200 }, + { + columnHeaderType: 'not-filtered', + displayAsText: 'Rule', + id: 'kibana.alert.rule.name', + initialWidth: 180, + linkField: 'kibana.alert.rule.uuid', + }, + { + columnHeaderType: 'not-filtered', + displayAsText: 'Assignees', + id: 'kibana.alert.workflow_assignee_ids', + initialWidth: 190, + }, + ...basicBaseColumns, + ], + }; + expect(model).toEqual(expected); + }); + + test('returns correct model for Platinum license', () => { + const licenseServiceMock = createLicenseServiceMock(); + const model = getAlertsDefaultModel(licenseServiceMock); + + const expected = { + ...tableDefaults, + showCheckboxes: true, + columns: [ + { columnHeaderType: 'not-filtered', id: '@timestamp', initialWidth: 200 }, + { + columnHeaderType: 'not-filtered', + displayAsText: 'Rule', + id: 'kibana.alert.rule.name', + initialWidth: 180, + linkField: 'kibana.alert.rule.uuid', + }, + { + columnHeaderType: 'not-filtered', + displayAsText: 'Assignees', + id: 'kibana.alert.workflow_assignee_ids', + initialWidth: 190, + }, + ...platinumBaseColumns, + ], + }; + expect(model).toEqual(expected); + }); + }); + + describe('getAlertsPreviewDefaultModel', () => { + test('returns correct model for Basic license', () => { + const licenseServiceMock = createLicenseServiceMock(); + licenseServiceMock.isPlatinumPlus.mockReturnValue(false); + const model = getAlertsPreviewDefaultModel(licenseServiceMock); + + const expected = { + ...tableDefaults, + showCheckboxes: false, + defaultColumns: [ + { columnHeaderType: 'not-filtered', id: 'kibana.alert.original_time', initialWidth: 200 }, + ...basicBaseColumns, + ], + columns: [ + { columnHeaderType: 'not-filtered', id: 'kibana.alert.original_time', initialWidth: 200 }, + ...basicBaseColumns, + ], + sort: [ + { + columnId: 'kibana.alert.original_time', + columnType: 'date', + esTypes: ['date'], + sortDirection: 'desc', + }, + ], + }; + expect(model).toEqual(expected); + }); + + test('returns correct model for Platinum license', () => { + const licenseServiceMock = createLicenseServiceMock(); + const model = getAlertsPreviewDefaultModel(licenseServiceMock); + + const expected = { + ...tableDefaults, + showCheckboxes: false, + defaultColumns: [ + { columnHeaderType: 'not-filtered', id: 'kibana.alert.original_time', initialWidth: 200 }, + ...platinumBaseColumns, + ], + columns: [ + { columnHeaderType: 'not-filtered', id: 'kibana.alert.original_time', initialWidth: 200 }, + ...platinumBaseColumns, + ], + sort: [ + { + columnId: 'kibana.alert.original_time', + columnType: 'date', + esTypes: ['date'], + sortDirection: 'desc', + }, + ], + }; + expect(model).toEqual(expected); + }); + }); + // TODO: move these tests to ../timelines/components/timeline/body/events/event_column_view.tsx // describe.skip('getAlertActions', () => { // let setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index 6065e617c12546..9db07f48246520 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -192,7 +192,7 @@ export const getAlertsDefaultModel = (license?: LicenseService): SubsetDataTable export const getAlertsPreviewDefaultModel = (license?: LicenseService): SubsetDataTableModel => ({ ...getAlertsDefaultModel(license), - columns: getColumns(license), + columns: getRulePreviewColumns(license), defaultColumns: getRulePreviewColumns(license), sort: [ { diff --git a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts index 384c6bf955e51f..b22bf5e2ed4294 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts +++ b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts @@ -25,6 +25,13 @@ import { DEFAULT_TABLE_DATE_COLUMN_MIN_WIDTH, } from './translations'; +export const assigneesColumn: ColumnHeaderOptions = { + columnHeaderType: defaultColumnHeaderType, + displayAsText: i18n.ALERTS_HEADERS_ASSIGNEES, + id: 'kibana.alert.workflow_assignee_ids', + initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH, +}; + const getBaseColumns = ( license?: LicenseService ): Array< @@ -32,12 +39,6 @@ const getBaseColumns = ( > => { const isPlatinumPlus = license?.isPlatinumPlus?.() ?? false; return [ - { - columnHeaderType: defaultColumnHeaderType, - displayAsText: i18n.ALERTS_HEADERS_ASSIGNEES, - id: 'kibana.alert.workflow_assignee_ids', - initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH, - }, { columnHeaderType: defaultColumnHeaderType, displayAsText: i18n.ALERTS_HEADERS_SEVERITY, @@ -131,6 +132,7 @@ export const getColumns = ( initialWidth: DEFAULT_COLUMN_MIN_WIDTH, linkField: 'kibana.alert.rule.uuid', }, + assigneesColumn, ...getBaseColumns(license), ]; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_selector.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_selector.tsx index a50caf23847f88..a1f4446c005240 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_selector.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_selector.tsx @@ -51,6 +51,7 @@ export const AssetCriticalitySelector: React.FC = ({ entity }) => { return ( <> @@ -80,12 +81,10 @@ export const AssetCriticalitySelector: React.FC = ({ entity }) => { > - {criticality.status === 'update' && ( - - )} + diff --git a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts index f715169e2abb90..94f03268a23b20 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts @@ -16,6 +16,7 @@ import { addTableInStorage, migrateAlertTableStateToTriggerActionsState, migrateTriggerActionsVisibleColumnsAlertTable88xTo89, + addAssigneesSpecsToSecurityDataTableIfNeeded, } from '.'; import { mockDataTableModel, createSecuritySolutionStorageMock } from '../../../common/mock'; @@ -566,6 +567,12 @@ describe('SiemLocalStorage', () => { { columnHeaderType: 'not-filtered', id: 'file.name' }, { columnHeaderType: 'not-filtered', id: 'source.ip' }, { columnHeaderType: 'not-filtered', id: 'destination.ip' }, + { + columnHeaderType: 'not-filtered', + displayAsText: 'Assignees', + id: 'kibana.alert.workflow_assignee_ids', + initialWidth: 190, + }, ], defaultColumns: [ { columnHeaderType: 'not-filtered', id: '@timestamp', initialWidth: 200 }, @@ -600,6 +607,12 @@ describe('SiemLocalStorage', () => { { columnHeaderType: 'not-filtered', id: 'file.name' }, { columnHeaderType: 'not-filtered', id: 'source.ip' }, { columnHeaderType: 'not-filtered', id: 'destination.ip' }, + { + columnHeaderType: 'not-filtered', + displayAsText: 'Assignees', + id: 'kibana.alert.workflow_assignee_ids', + initialWidth: 190, + }, ], dataViewId: 'security-solution-default', deletedEventIds: [], @@ -1527,4 +1540,88 @@ describe('SiemLocalStorage', () => { ).toBeNull(); }); }); + + describe('addMissingColumnsToSecurityDataTable', () => { + it('should add missing "Assignees" column specs', () => { + const dataTableState: DataTableState['dataTable']['tableById'] = { + 'alerts-page': { + columns: [{ columnHeaderType: 'not-filtered', id: '@timestamp', initialWidth: 200 }], + defaultColumns: [ + { columnHeaderType: 'not-filtered', id: 'host.name' }, + { columnHeaderType: 'not-filtered', id: 'user.name' }, + { columnHeaderType: 'not-filtered', id: 'process.name' }, + ], + isLoading: false, + queryFields: [], + dataViewId: 'security-solution-default', + deletedEventIds: [], + expandedDetail: {}, + filters: [], + indexNames: ['.alerts-security.alerts-default'], + isSelectAllChecked: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50, 100], + loadingEventIds: [], + showCheckboxes: true, + sort: [ + { + columnId: '@timestamp', + columnType: 'date', + esTypes: ['date'], + sortDirection: 'desc', + }, + ], + graphEventId: undefined, + selectedEventIds: {}, + sessionViewConfig: null, + selectAll: false, + id: 'alerts-page', + title: '', + initialized: true, + updated: 1665943295913, + totalCount: 0, + viewMode: VIEW_SELECTION.gridView, + additionalFilters: { + showBuildingBlockAlerts: false, + showOnlyThreatIndicatorAlerts: false, + }, + }, + }; + storage.set(LOCAL_STORAGE_TABLE_KEY, dataTableState); + migrateAlertTableStateToTriggerActionsState(storage, dataTableState); + migrateTriggerActionsVisibleColumnsAlertTable88xTo89(storage); + + const expectedColumns = [ + { columnHeaderType: 'not-filtered', id: '@timestamp', initialWidth: 200 }, + { + columnHeaderType: 'not-filtered', + displayAsText: 'Assignees', + id: 'kibana.alert.workflow_assignee_ids', + initialWidth: 190, + }, + ]; + const expectedDefaultColumns = [ + { columnHeaderType: 'not-filtered', id: 'host.name' }, + { columnHeaderType: 'not-filtered', id: 'user.name' }, + { columnHeaderType: 'not-filtered', id: 'process.name' }, + { + columnHeaderType: 'not-filtered', + displayAsText: 'Assignees', + id: 'kibana.alert.workflow_assignee_ids', + initialWidth: 190, + }, + ]; + + addAssigneesSpecsToSecurityDataTableIfNeeded(storage, dataTableState); + + expect(dataTableState['alerts-page'].columns).toMatchObject(expectedColumns); + expect(dataTableState['alerts-page'].defaultColumns).toMatchObject(expectedDefaultColumns); + + const tableKey = 'detection-engine-alert-table-securitySolution-alerts-page-gridView'; + expect(storage.get(tableKey)).toMatchObject({ + columns: expectedColumns, + visibleColumns: expectedColumns.map((col) => col.id), + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx index bab84294e0a676..8191f800ff9349 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx @@ -12,8 +12,9 @@ import type { DataTableModel, TableIdLiteral, } from '@kbn/securitysolution-data-table'; -import { TableId } from '@kbn/securitysolution-data-table'; +import { tableEntity, TableEntityType, TableId } from '@kbn/securitysolution-data-table'; import type { ColumnHeaderOptions } from '@kbn/timelines-plugin/common'; +import { assigneesColumn } from '../../../detections/configurations/security_solution_detections/columns'; import { ALERTS_TABLE_REGISTRY_CONFIG_IDS, VIEW_SELECTION } from '../../../../common/constants'; import type { DataTablesStorage } from './types'; import { useKibana } from '../../../common/lib/kibana'; @@ -195,6 +196,90 @@ export const migrateColumnLabelToDisplayAsText = ( : {}), }); +/** + * Adds "Assignees" column and makes it visible in alerts table + */ +const addAssigneesColumnToAlertsTable = (storage: Storage) => { + const localStorageKeys = [ + `detection-engine-alert-table-${ALERTS_TABLE_REGISTRY_CONFIG_IDS.ALERTS_PAGE}-gridView`, + `detection-engine-alert-table-${ALERTS_TABLE_REGISTRY_CONFIG_IDS.RULE_DETAILS}-gridView`, + ]; + + localStorageKeys.forEach((key) => { + const alertTableData = storage.get(key); + if (!alertTableData) { + return; + } + // Make "Assignees" field selected in the table + if ('columns' in alertTableData) { + let updatedAlertsTableState = false; + const columns = + alertTableData.columns as DataTableState['dataTable']['tableById'][string]['columns']; + const hasAssigneesColumn = columns.findIndex((col) => col.id === assigneesColumn.id) !== -1; + if (!hasAssigneesColumn) { + // Insert "Assignees" column at the index 1 to mimic behaviour of adding field to alerts table + alertTableData.columns.splice(1, 0, assigneesColumn); + updatedAlertsTableState = true; + } + // Make "Assignees" column visible in the table + if ('visibleColumns' in alertTableData) { + const visibleColumns = alertTableData.visibleColumns as string[]; + const assigneesColumnExists = + visibleColumns.findIndex((col) => col === assigneesColumn.id) !== -1; + if (!assigneesColumnExists) { + alertTableData.visibleColumns.splice(1, 0, assigneesColumn.id); + updatedAlertsTableState = true; + } + } + if (updatedAlertsTableState) { + storage.set(key, alertTableData); + } + } + }); +}; + +/** + * Adds "Assignees" column specs to table data model + */ +export const addAssigneesSpecsToSecurityDataTableIfNeeded = ( + storage: Storage, + dataTableState: DataTableState['dataTable']['tableById'] +) => { + // Add "Assignees" column specs to the table data model + let updatedTableModel = false; + for (const [tableId, tableModel] of Object.entries(dataTableState)) { + // Only add "Assignees" column specs to alerts tables + if (tableEntity[tableId as TableId] !== TableEntityType.alert) { + // eslint-disable-next-line no-continue + continue; + } + + // We added a new base column for "Assignees" in 8.12 + // In order to show correct custom header label after user upgrades to 8.12 we need to make sure the appropriate specs are in the table model. + const columns = tableModel.columns; + if (Array.isArray(columns)) { + const hasAssigneesColumn = columns.findIndex((col) => col.id === assigneesColumn.id) !== -1; + if (!hasAssigneesColumn) { + updatedTableModel = true; + tableModel.columns.push(assigneesColumn); + } + } + const defaultColumns = tableModel.defaultColumns; + if (defaultColumns) { + const hasAssigneesColumn = + defaultColumns.findIndex((col) => col.id === assigneesColumn.id) !== -1; + if (!hasAssigneesColumn) { + updatedTableModel = true; + tableModel.defaultColumns.push(assigneesColumn); + } + } + } + if (updatedTableModel) { + storage.set(LOCAL_STORAGE_TABLE_KEY, dataTableState); + addAssigneesColumnToAlertsTable(storage); + } +}; + export const getDataTablesInStorageByIds = (storage: Storage, tableIds: TableIdLiteral[]) => { let allDataTables = storage.get(LOCAL_STORAGE_TABLE_KEY); const legacyTimelineTables = storage.get(LOCAL_STORAGE_TIMELINE_KEY_LEGACY); @@ -209,6 +294,7 @@ export const getDataTablesInStorageByIds = (storage: Storage, tableIds: TableIdL migrateAlertTableStateToTriggerActionsState(storage, allDataTables); migrateTriggerActionsVisibleColumnsAlertTable88xTo89(storage); + addAssigneesSpecsToSecurityDataTableIfNeeded(storage, allDataTables); return tableIds.reduce((acc, tableId) => { const tableModel = allDataTables[tableId]; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 2598405f9e40f8..4db06e42866699 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -7936,7 +7936,6 @@ "xpack.apm.anomalyDetection.createJobs.failed.text": "Une erreur est survenue lors de la création d'une ou de plusieurs tâches de détection des anomalies pour les environnements de service APM [{environments}]. Erreur : \"{errorMessage}\"", "xpack.apm.anomalyDetection.createJobs.succeeded.text": "Tâches de détection des anomalies créées avec succès pour les environnements de service APM [{environments}]. Le démarrage de l'analyse du trafic à la recherche d'anomalies par le Machine Learning va prendre un certain temps.", "xpack.apm.anomalyDetectionSetup.notEnabledForEnvironmentText": "La détection des anomalies n'est pas encore activée pour l'environnement \"{currentEnvironment}\". Cliquez pour continuer la configuration.", - "xpack.apm.bottomBarActions.unsavedChanges": "{unsavedChangesCount, plural, =0 {0 modification non enregistrée} one {1 modification non enregistrée} many {# modifications non enregistrées} other {# modifications non enregistrées}} ", "xpack.apm.compositeSpanCallsLabel": ", {count} appels, sur une moyenne de {duration}", "xpack.apm.correlations.ccsWarningCalloutBody": "Les données pour l'analyse de corrélation n'ont pas pu être totalement récupérées. Cette fonctionnalité est prise en charge uniquement pour {version} et versions ultérieures.", "xpack.apm.correlations.failedTransactions.helpPopover.basicExplanation": "Les corrélations vous aident à découvrir les attributs qui ont le plus d'influence pour distinguer les échecs et les succès d'une transaction. Les transactions sont considérées comme un échec lorsque leur valeur {field} est {value}.", @@ -8360,7 +8359,6 @@ "xpack.apm.appName": "APM", "xpack.apm.betaBadgeDescription": "Cette fonctionnalité est actuellement en version bêta. Si vous rencontrez des bugs ou si vous souhaitez apporter des commentaires, ouvrez un ticket de problème ou visitez notre forum de discussion.", "xpack.apm.betaBadgeLabel": "Bêta", - "xpack.apm.bottomBarActions.discardChangesButton": "Abandonner les modifications", "xpack.apm.chart.annotation.version": "Version", "xpack.apm.chart.comparison.defaultPreviousPeriodLabel": "Période précédente", "xpack.apm.chart.cpuSeries.processAverageLabel": "Moyenne de processus", @@ -20683,6 +20681,7 @@ "xpack.infra.sourceConfiguration.noRemoteClusterTitle": "Impossible de se connecter au serveur distant", "xpack.infra.sourceConfiguration.remoteClusterConnectionDoNotExist": "Vérifiez que le cluster distant est disponible ou que les paramètres de connexion à distance sont corrects.", "xpack.infra.sourceConfiguration.remoteClusterConnectionDoNotExistTitle": "Impossible de se connecter au cluster distant", + "xpack.infra.sourceConfiguration.saveButton": "Enregistrer les modifications", "xpack.infra.sourceConfiguration.systemColumnBadgeLabel": "Système", "xpack.infra.sourceConfiguration.unsavedFormPrompt": "Voulez-vous vraiment quitter ? Les modifications seront perdues", "xpack.infra.sourceConfiguration.updateFailureBody": "Nous n'avons pas pu appliquer les modifications à la configuration des indicateurs. Réessayez plus tard.", @@ -20786,6 +20785,98 @@ "xpack.infra.waffle.unableToSelectMetricErrorTitle": "Impossible de sélectionner les options ou la valeur pour l'indicateur.", "xpack.infra.waffleTime.autoRefreshButtonLabel": "Actualisation automatique", "xpack.infra.waffleTime.stopRefreshingButtonLabel": "Arrêter l'actualisation", + "xpack.observabilityShared.featureFeedbackButton.tellUsWhatYouThinkLink": "Dites-nous ce que vous pensez !", + "xpack.observabilityShared.bottomBarActions.discardChangesButton": "Abandonner les modifications", + "xpack.observabilityShared.bottomBarActions.unsavedChanges": "{unsavedChangesCount, plural, =0 {0 modification non enregistrée} one {1 modification non enregistrée} many {# modifications non enregistrées} other {# modifications non enregistrées}} ", + "xpack.observabilityShared.breadcrumbs.observabilityLinkText": "Observabilité", + "xpack.observabilityShared.inspector.stats.queryTimeValue": "{queryTime} ms", + "xpack.observabilityShared.inspector.stats.dataViewDescription": "La vue de données qui se connecte aux index Elasticsearch.", + "xpack.observabilityShared.inspector.stats.dataViewLabel": "Vue de données", + "xpack.observabilityShared.inspector.stats.hitsDescription": "Le nombre de documents renvoyés par la requête.", + "xpack.observabilityShared.inspector.stats.hitsLabel": "Résultats", + "xpack.observabilityShared.inspector.stats.hitsTotalDescription": "Le nombre de documents correspondant à la requête.", + "xpack.observabilityShared.inspector.stats.hitsTotalLabel": "Résultats (total)", + "xpack.observabilityShared.inspector.stats.kibanaApiQueryParametersDescription": "Les paramètres de requête utilisés dans la requête d'API Kibana à l'origine de la requête Elasticsearch.", + "xpack.observabilityShared.inspector.stats.kibanaApiQueryParametersLabel": "Paramètres de requête d'API Kibana", + "xpack.observabilityShared.inspector.stats.kibanaApiRouteDescription": "Le chemin de la requête d'API Kibana à l'origine de la requête Elasticsearch.", + "xpack.observabilityShared.inspector.stats.kibanaApiRouteLabel": "Chemin de l’API Kibana", + "xpack.observabilityShared.inspector.stats.queryTimeDescription": "Le temps qu'il a fallu pour traiter la requête. Ne comprend pas le temps nécessaire pour envoyer la requête ni l'analyser dans le navigateur.", + "xpack.observabilityShared.inspector.stats.queryTimeLabel": "Durée de la requête", + "xpack.observabilityShared.navigation.betaBadge": "Bêta", + "xpack.observabilityShared.navigation.experimentalBadgeLabel": "Version d'évaluation technique", + "xpack.observabilityShared.navigation.newBadge": "NOUVEAUTÉ", + "xpack.observabilityShared.pageLayout.sideNavTitle": "Observabilité", + "xpack.observabilityShared.sectionLink.newLabel": "Nouveauté", + "xpack.observabilityShared.technicalPreviewBadgeDescription": "Cette fonctionnalité est en version d'évaluation technique et pourra être modifiée ou retirée complètement dans une future version. Elastic s'efforcera au maximum de corriger tout problème, mais les fonctionnalités en version d'évaluation technique ne sont pas soumises aux accords de niveau de service d'assistance des fonctionnalités officielles en disponibilité générale.", + "xpack.observabilityShared.technicalPreviewBadgeLabel": "Version d'évaluation technique", + "xpack.observabilityShared.tour.alertsStep.imageAltText": "Démonstration des alertes", + "xpack.observabilityShared.tour.alertsStep.tourContent": "Définissez et détectez les conditions qui déclenchent des alertes avec des intégrations de plateformes tierces comme l’e-mail, PagerDuty et Slack.", + "xpack.observabilityShared.tour.alertsStep.tourTitle": "Soyez informé en cas de modification", + "xpack.observabilityShared.tour.endButtonLabel": "Terminer la visite", + "xpack.observabilityShared.tour.guidedSetupStep.tourContent": "La façon la plus facile de continuer avec Elastic Observability est de suivre les prochaines étapes recommandées dans l'assistant de données.", + "xpack.observabilityShared.tour.guidedSetupStep.tourTitle": "Toujours plus avec Elastic Observability", + "xpack.observabilityShared.tour.metricsExplorerStep.imageAltText": "Démonstration de Metrics Explorer", + "xpack.observabilityShared.tour.metricsExplorerStep.tourContent": "Diffusez, regroupez et visualisez les mesures provenant de vos systèmes, du cloud, du réseau et d'autres sources d'infrastructure.", + "xpack.observabilityShared.tour.metricsExplorerStep.tourTitle": "Monitorer l’intégrité de votre infrastructure", + "xpack.observabilityShared.tour.nextButtonLabel": "Suivant", + "xpack.observabilityShared.tour.observabilityOverviewStep.tourContent": "Faites un tour rapide pour découvrir les avantages de disposer de toutes vos données d'observabilité dans une seule suite.", + "xpack.observabilityShared.tour.observabilityOverviewStep.tourTitle": "Bienvenue dans Elastic Observability", + "xpack.observabilityShared.tour.servicesStep.imageAltText": "Démonstration des services", + "xpack.observabilityShared.tour.servicesStep.tourContent": "Détectez et réparez rapidement les problèmes de performances en recueillant des informations détaillées sur vos services.", + "xpack.observabilityShared.tour.servicesStep.tourTitle": "Identifier et résoudre les problèmes d'application", + "xpack.observabilityShared.tour.skipButtonLabel": "Ignorer la visite", + "xpack.observabilityShared.tour.streamStep.imageAltText": "Démonstration du flux de logs", + "xpack.observabilityShared.tour.streamStep.tourContent": "Surveillez, filtrez et inspectez les événements de journal provenant de vos applications, serveurs, machines virtuelles et conteneurs.", + "xpack.observabilityShared.tour.streamStep.tourTitle": "Suivi de vos logs en temps réel", + "xpack.metricsData.assetDetails.formulas.cpuUsage": "Utilisation CPU", + "xpack.metricsData.assetDetails.formulas.cpuUsage.iowaitLabel": "iowait", + "xpack.metricsData.assetDetails.formulas.cpuUsage.irqLabel": "irq", + "xpack.metricsData.assetDetails.formulas.cpuUsage.niceLabel": "nice", + "xpack.metricsData.assetDetails.formulas.cpuUsage.softirqLabel": "softirq", + "xpack.metricsData.assetDetails.formulas.cpuUsage.stealLabel": "steal", + "xpack.metricsData.assetDetails.formulas.cpuUsage.systemLabel": "system", + "xpack.metricsData.assetDetails.formulas.cpuUsage.userLabel": "utilisateur", + "xpack.metricsData.assetDetails.formulas.diskIORead": "Entrées et sorties par seconde en lecture sur le disque", + "xpack.metricsData.assetDetails.formulas.diskIOWrite": "Entrées et sorties par seconde en écriture sur le disque", + "xpack.metricsData.assetDetails.formulas.diskReadThroughput": "Rendement de lecture du disque", + "xpack.metricsData.assetDetails.formulas.diskSpaceAvailability": "Disponibilité de l'espace disque", + "xpack.metricsData.assetDetails.formulas.diskSpaceAvailable": "Espace disque disponible", + "xpack.metricsData.assetDetails.formulas.diskUsage": "Utilisation du disque", + "xpack.metricsData.assetDetails.formulas.diskWriteThroughput": "Rendement d’écriture du disque", + "xpack.metricsData.assetDetails.formulas.hostCount.hostsLabel": "Hôtes", + "xpack.metricsData.assetDetails.formulas.kubernetes.capacity": "Capacité", + "xpack.metricsData.assetDetails.formulas.kubernetes.used": "Utilisé", + "xpack.metricsData.assetDetails.formulas.load15m": "Charge (15 min)", + "xpack.metricsData.assetDetails.formulas.load1m": "Charge (1 min)", + "xpack.metricsData.assetDetails.formulas.load5m": "Charge (5 min)", + "xpack.metricsData.assetDetails.formulas.logRate": "Taux de log", + "xpack.metricsData.assetDetails.formulas.memoryFree": "Sans mémoire", + "xpack.metricsData.assetDetails.formulas.memoryUsage": "Utilisation mémoire", + "xpack.metricsData.assetDetails.formulas.metric.label.cache": "cache", + "xpack.metricsData.assetDetails.formulas.metric.label.free": "gratuit", + "xpack.metricsData.assetDetails.formulas.metric.label.used": "utilisé", + "xpack.metricsData.assetDetails.formulas.normalizedLoad1m": "Charge normalisée", + "xpack.metricsData.assetDetails.formulas.rx": "Réseau entrant (RX)", + "xpack.metricsData.assetDetails.formulas.tx": "Réseau sortant (TX)", + "xpack.metricsData.assetDetails.metricsCharts.diskIOPS": "Entrées et sorties par seconde (IOPS) sur le disque", + "xpack.metricsData.assetDetails.metricsCharts.diskThroughput": "Rendement du disque", + "xpack.metricsData.assetDetails.metricsCharts.diskUsage.label.available": "Disponible", + "xpack.metricsData.assetDetails.metricsCharts.diskUsage.label.used": "Utilisé", + "xpack.metricsData.assetDetails.metricsCharts.diskUsageByMountingPoint": "Utilisation du disque par point de montage", + "xpack.metricsData.assetDetails.metricsCharts.kubernetes.nodeCpuCapacity": "Capacité CPU du nœud", + "xpack.metricsData.assetDetails.metricsCharts.load": "Charge", + "xpack.metricsData.assetDetails.metricsCharts.metric.label.cache": "Cache", + "xpack.metricsData.assetDetails.metricsCharts.metric.label.free": "Gratuit", + "xpack.metricsData.assetDetails.metricsCharts.metric.label.read": "Lire", + "xpack.metricsData.assetDetails.metricsCharts.metric.label.used": "Utilisé", + "xpack.metricsData.assetDetails.metricsCharts.metric.label.write": "Écrire", + "xpack.metricsData.assetDetails.metricsCharts.network": "Réseau", + "xpack.metricsData.assetDetails.metricsCharts.network.label.rx": "Entrant (RX)", + "xpack.metricsData.assetDetails.metricsCharts.network.label.tx": "Sortant (TX)", + "xpack.metricsData.assetDetails.metricsCharts.kubernetes.nodeDiskCapacity": "Capacité du disque du nœud", + "xpack.metricsData.assetDetails.metricsCharts.kubernetes.nodeMemoryCapacity": "Capacité de mémoire du nœud", + "xpack.metricsData.assetDetails.metricsCharts.kubernetes.nodePodCapacity": "Capacité de pod du nœud", + "xpack.metricsData.assetDetails.overview.kpi.subtitle.average": "Moyenne", "xpack.ingestPipelines.app.deniedPrivilegeDescription": "Pour utiliser l'option Ingérer des pipelines, vous devez avoir {privilegesCount, plural, one {ce privilège de cluster} many {ces privilèges de cluster} other {ces privilèges de cluster}} : {missingPrivileges}.", "xpack.ingestPipelines.clone.loadSourcePipelineErrorTitle": "Impossible de charger {name}.", "xpack.ingestPipelines.createFromCsv.errorMessage": "{message}", @@ -29286,46 +29377,6 @@ "xpack.observabilityAiAssistant.setupKb": "Améliorez votre expérience en configurant la base de connaissances.", "xpack.observabilityAiAssistant.stopGeneratingButtonLabel": "Arrêter la génération", "xpack.observabilityAiAssistant.technicalPreviewBadgeDescription": "GTP4 est nécessaire pour bénéficier d'une meilleure expérience avec les appels de fonctions (par exemple lors de la réalisation d'analyse de la cause d'un problème, de la visualisation de données et autres). GPT3.5 peut fonctionner pour certains des workflows les plus simples comme les explications d'erreurs ou pour bénéficier d'une expérience comparable à ChatGPT au sein de Kibana à partir du moment où les appels de fonctions ne sont pas fréquents.", - "xpack.observabilityShared.inspector.stats.queryTimeValue": "{queryTime} ms", - "xpack.observabilityShared.breadcrumbs.observabilityLinkText": "Observabilité", - "xpack.observabilityShared.inspector.stats.dataViewDescription": "La vue de données qui se connecte aux index Elasticsearch.", - "xpack.observabilityShared.inspector.stats.dataViewLabel": "Vue de données", - "xpack.observabilityShared.inspector.stats.hitsDescription": "Le nombre de documents renvoyés par la requête.", - "xpack.observabilityShared.inspector.stats.hitsLabel": "Résultats", - "xpack.observabilityShared.inspector.stats.hitsTotalDescription": "Le nombre de documents correspondant à la requête.", - "xpack.observabilityShared.inspector.stats.hitsTotalLabel": "Résultats (total)", - "xpack.observabilityShared.inspector.stats.kibanaApiQueryParametersDescription": "Les paramètres de requête utilisés dans la requête d'API Kibana à l'origine de la requête Elasticsearch.", - "xpack.observabilityShared.inspector.stats.kibanaApiQueryParametersLabel": "Paramètres de requête d'API Kibana", - "xpack.observabilityShared.inspector.stats.kibanaApiRouteDescription": "Le chemin de la requête d'API Kibana à l'origine de la requête Elasticsearch.", - "xpack.observabilityShared.inspector.stats.kibanaApiRouteLabel": "Chemin de l’API Kibana", - "xpack.observabilityShared.inspector.stats.queryTimeDescription": "Le temps qu'il a fallu pour traiter la requête. Ne comprend pas le temps nécessaire pour envoyer la requête ni l'analyser dans le navigateur.", - "xpack.observabilityShared.inspector.stats.queryTimeLabel": "Durée de la requête", - "xpack.observabilityShared.navigation.betaBadge": "Bêta", - "xpack.observabilityShared.navigation.experimentalBadgeLabel": "Version d'évaluation technique", - "xpack.observabilityShared.navigation.newBadge": "NOUVEAUTÉ", - "xpack.observabilityShared.pageLayout.sideNavTitle": "Observabilité", - "xpack.observabilityShared.sectionLink.newLabel": "Nouveauté", - "xpack.observabilityShared.technicalPreviewBadgeDescription": "Cette fonctionnalité est en version d'évaluation technique et pourra être modifiée ou retirée complètement dans une future version. Elastic s'efforcera au maximum de corriger tout problème, mais les fonctionnalités en version d'évaluation technique ne sont pas soumises aux accords de niveau de service d'assistance des fonctionnalités officielles en disponibilité générale.", - "xpack.observabilityShared.technicalPreviewBadgeLabel": "Version d'évaluation technique", - "xpack.observabilityShared.tour.alertsStep.imageAltText": "Démonstration des alertes", - "xpack.observabilityShared.tour.alertsStep.tourContent": "Définissez et détectez les conditions qui déclenchent des alertes avec des intégrations de plateformes tierces comme l’e-mail, PagerDuty et Slack.", - "xpack.observabilityShared.tour.alertsStep.tourTitle": "Soyez informé en cas de modification", - "xpack.observabilityShared.tour.endButtonLabel": "Terminer la visite", - "xpack.observabilityShared.tour.guidedSetupStep.tourContent": "La façon la plus facile de continuer avec Elastic Observability est de suivre les prochaines étapes recommandées dans l'assistant de données.", - "xpack.observabilityShared.tour.guidedSetupStep.tourTitle": "Toujours plus avec Elastic Observability", - "xpack.observabilityShared.tour.metricsExplorerStep.imageAltText": "Démonstration de Metrics Explorer", - "xpack.observabilityShared.tour.metricsExplorerStep.tourContent": "Diffusez, regroupez et visualisez les mesures provenant de vos systèmes, du cloud, du réseau et d'autres sources d'infrastructure.", - "xpack.observabilityShared.tour.metricsExplorerStep.tourTitle": "Monitorer l’intégrité de votre infrastructure", - "xpack.observabilityShared.tour.nextButtonLabel": "Suivant", - "xpack.observabilityShared.tour.observabilityOverviewStep.tourContent": "Faites un tour rapide pour découvrir les avantages de disposer de toutes vos données d'observabilité dans une seule suite.", - "xpack.observabilityShared.tour.observabilityOverviewStep.tourTitle": "Bienvenue dans Elastic Observability", - "xpack.observabilityShared.tour.servicesStep.imageAltText": "Démonstration des services", - "xpack.observabilityShared.tour.servicesStep.tourContent": "Détectez et réparez rapidement les problèmes de performances en recueillant des informations détaillées sur vos services.", - "xpack.observabilityShared.tour.servicesStep.tourTitle": "Identifier et résoudre les problèmes d'application", - "xpack.observabilityShared.tour.skipButtonLabel": "Ignorer la visite", - "xpack.observabilityShared.tour.streamStep.imageAltText": "Démonstration du flux de logs", - "xpack.observabilityShared.tour.streamStep.tourContent": "Surveillez, filtrez et inspectez les événements de journal provenant de vos applications, serveurs, machines virtuelles et conteneurs.", - "xpack.observabilityShared.tour.streamStep.tourTitle": "Suivi de vos logs en temps réel", "xpack.osquery.action.missingPrivileges": "Pour accéder à cette page, demandez à votre administrateur vos privilèges Kibana pour {osquery}.", "xpack.osquery.agentPolicy.confirmModalCalloutDescription": "Fleet a détecté que {agentPolicyCount, plural, one {politique d''agent} many {ces politiques d''agent} other {les politiques d''agent sélectionnées sont}} déjà en cours d'utilisation par certains de vos agents. Suite à cette action, Fleet déploie les mises à jour de tous les agents qui utilisent {agentPolicyCount, plural, one {politique d''agent} many {ces politiques d''agent} other {ces politiques d''agent}}.", "xpack.osquery.agentPolicy.confirmModalCalloutTitle": "Cette action va mettre à jour {agentCount, plural, one {# agent} many {# agents ont été enregistrés} other {# agents}}", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2243248e76da19..83f07af398cf29 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7951,7 +7951,6 @@ "xpack.apm.anomalyDetection.createJobs.failed.text": "APMサービス環境[{environments}]用に1つ以上の異常検知ジョブを作成しているときに問題が発生しました。エラー「{errorMessage}」", "xpack.apm.anomalyDetection.createJobs.succeeded.text": "APMサービス環境[{environments}]の異常検知ジョブが正常に作成されました。機械学習がトラフィック異常値の分析を開始するには、少し時間がかかります。", "xpack.apm.anomalyDetectionSetup.notEnabledForEnvironmentText": "「{currentEnvironment}」環境では、まだ異常検知が有効ではありません。クリックすると、セットアップを続行します。", - "xpack.apm.bottomBarActions.unsavedChanges": "{unsavedChangesCount, plural, =0 {0個の保存されていない変更} other {#個の保存されていない変更}} ", "xpack.apm.compositeSpanCallsLabel": "、{count}件の呼び出し、平均:{duration}", "xpack.apm.correlations.ccsWarningCalloutBody": "相関関係分析のデータを完全に取得できませんでした。この機能はバージョン{version}以降でのみサポートされています。", "xpack.apm.correlations.failedTransactions.helpPopover.basicExplanation": "相関関係では、トランザクションの失敗と成功を区別するうえで最も影響度が大きい属性を見つけることができます。{field}値が{value}のときには、トランザクションが失敗であると見なされます。", @@ -8375,7 +8374,6 @@ "xpack.apm.appName": "APM", "xpack.apm.betaBadgeDescription": "現在、この機能はベータです。不具合を見つけた場合やご意見がある場合、サポートに問い合わせるか、またはディスカッションフォーラムにご報告ください。", "xpack.apm.betaBadgeLabel": "ベータ", - "xpack.apm.bottomBarActions.discardChangesButton": "変更を破棄", "xpack.apm.chart.annotation.version": "バージョン", "xpack.apm.chart.comparison.defaultPreviousPeriodLabel": "前の期間", "xpack.apm.chart.cpuSeries.processAverageLabel": "プロセス平均", @@ -20696,6 +20694,7 @@ "xpack.infra.sourceConfiguration.noRemoteClusterTitle": "リモートクラスターに接続できませんでした", "xpack.infra.sourceConfiguration.remoteClusterConnectionDoNotExist": "リモートクラスターが利用可能であるか、リモート接続の設定が正しいことを確認します。", "xpack.infra.sourceConfiguration.remoteClusterConnectionDoNotExistTitle": "リモートクラスターに接続できませんでした", + "xpack.infra.sourceConfiguration.saveButton": "変更を保存", "xpack.infra.sourceConfiguration.systemColumnBadgeLabel": "システム", "xpack.infra.sourceConfiguration.unsavedFormPrompt": "終了してよろしいですか?変更内容は失われます", "xpack.infra.sourceConfiguration.updateFailureBody": "変更をメトリック構成に適用できませんでした。しばらくたってから再試行してください。", @@ -20799,6 +20798,99 @@ "xpack.infra.waffle.unableToSelectMetricErrorTitle": "メトリックのオプションまたは値を選択できません。", "xpack.infra.waffleTime.autoRefreshButtonLabel": "自動更新", "xpack.infra.waffleTime.stopRefreshingButtonLabel": "更新中止", + "xpack.observabilityShared.featureFeedbackButton.tellUsWhatYouThinkLink": "ご意見をお聞かせください。", + "xpack.observabilityShared.bottomBarActions.unsavedChanges": "{unsavedChangesCount, plural, =0 {0個の保存されていない変更} other {#個の保存されていない変更}} ", + "xpack.observabilityShared.bottomBarActions.discardChangesButton": "変更を破棄", + "xpack.observabilityShared.breadcrumbs.observabilityLinkText": "Observability", + "xpack.observabilityShared.inspector.stats.queryTimeValue": "{queryTime}ms", + "xpack.observabilityShared.inspector.stats.dataViewDescription": "Elasticsearchインデックスに接続したデータビューです。", + "xpack.observabilityShared.inspector.stats.dataViewLabel": "データビュー", + "xpack.observabilityShared.inspector.stats.hitsDescription": "クエリにより返されたドキュメントの数です。", + "xpack.observabilityShared.inspector.stats.hitsLabel": "ヒット数", + "xpack.observabilityShared.inspector.stats.hitsTotalDescription": "クエリに一致するドキュメントの数です。", + "xpack.observabilityShared.inspector.stats.hitsTotalLabel": "ヒット数(合計)", + "xpack.observabilityShared.inspector.stats.kibanaApiQueryParametersDescription": "Elasticsearch要求を開始したKibana API要求で使用されているクエリパラメーター。", + "xpack.observabilityShared.inspector.stats.kibanaApiQueryParametersLabel": "Kibana APIクエリパラメーター", + "xpack.observabilityShared.inspector.stats.kibanaApiRouteDescription": "Elasticsearch要求を開始したKibana API要求のルート。", + "xpack.observabilityShared.inspector.stats.kibanaApiRouteLabel": "Kibana APIルート", + "xpack.observabilityShared.inspector.stats.queryTimeDescription": "クエリの処理の所要時間です。リクエストの送信やブラウザーでのパースの時間は含まれません。", + "xpack.observabilityShared.inspector.stats.queryTimeLabel": "クエリ時間", + "xpack.observabilityShared.navigation.betaBadge": "ベータ", + "xpack.observabilityShared.navigation.experimentalBadgeLabel": "テクニカルプレビュー", + "xpack.observabilityShared.navigation.newBadge": "新規", + "xpack.observabilityShared.pageLayout.sideNavTitle": "Observability", + "xpack.observabilityShared.sectionLink.newLabel": "新規", + "xpack.observabilityShared.technicalPreviewBadgeDescription": "この機能はテクニカルプレビュー中であり、将来のリリースでは変更されたり完全に削除されたりする場合があります。Elasticは最善の努力を講じてすべての問題の修正に努めますが、テクニカルプレビュー中の機能には正式なGA機能のサポートSLAが適用されません。", + "xpack.observabilityShared.technicalPreviewBadgeLabel": "テクニカルプレビュー", + "xpack.observabilityShared.tour.alertsStep.imageAltText": "アラートデモ", + "xpack.observabilityShared.tour.alertsStep.tourContent": "電子メール、PagerDuty、Slackなどのサードパーティプラットフォーム統合でアラートをトリガーする条件を定義して検出します。", + "xpack.observabilityShared.tour.alertsStep.tourTitle": "変更が発生したときに通知", + "xpack.observabilityShared.tour.endButtonLabel": "ツアーを終了", + "xpack.observabilityShared.tour.guidedSetupStep.tourContent": "Elasticオブザーバビリティに進む最も簡単な方法は、データアシスタントで推奨された次のステップに従うことです。", + "xpack.observabilityShared.tour.guidedSetupStep.tourTitle": "Elasticオブザーバビリティのその他の機能", + "xpack.observabilityShared.tour.metricsExplorerStep.imageAltText": "メトリックエクスプローラーのデモ", + "xpack.observabilityShared.tour.metricsExplorerStep.tourContent": "システム、クラウド、ネットワーク、その他のインフラストラクチャーソースからメトリックをストリーム、グループ化、可視化します。", + "xpack.observabilityShared.tour.metricsExplorerStep.tourTitle": "インフラストラクチャーの正常性を監視", + "xpack.observabilityShared.tour.nextButtonLabel": "次へ", + "xpack.observabilityShared.tour.observabilityOverviewStep.tourContent": "クイックガイドを表示し、オブザーバビリティデータすべてを1つのスタックに格納する利点をご覧ください。", + "xpack.observabilityShared.tour.observabilityOverviewStep.tourTitle": "Elasticオブザーバビリティへようこそ", + "xpack.observabilityShared.tour.servicesStep.imageAltText": "サービスのデモ", + "xpack.observabilityShared.tour.servicesStep.tourContent": "サービスに関する詳細情報を収集し、パフォーマンスの問題をすばやく検出、修正できます。", + "xpack.observabilityShared.tour.servicesStep.tourTitle": "アプリケーションの問題を特定して解決", + "xpack.observabilityShared.tour.skipButtonLabel": "ツアーをスキップ", + "xpack.observabilityShared.tour.streamStep.imageAltText": "ログストリームのデモ", + "xpack.observabilityShared.tour.streamStep.tourContent": "アプリケーション、サーバー、仮想マシン、コネクターからのログイベントを監視、フィルター、検査します。", + "xpack.observabilityShared.tour.streamStep.tourTitle": "リアルタイムでログを追跡", + "xpack.metricsData.assetDetails.formulas.cpuUsage": "CPU使用状況", + "xpack.metricsData.assetDetails.formulas.cpuUsage.iowaitLabel": "iowait", + "xpack.metricsData.assetDetails.formulas.cpuUsage.irqLabel": "irq", + "xpack.metricsData.assetDetails.formulas.cpuUsage.niceLabel": "nice", + "xpack.metricsData.assetDetails.formulas.cpuUsage.softirqLabel": "softirq", + "xpack.metricsData.assetDetails.formulas.cpuUsage.stealLabel": "steal", + "xpack.metricsData.assetDetails.formulas.cpuUsage.systemLabel": "システム", + "xpack.metricsData.assetDetails.formulas.cpuUsage.userLabel": "ユーザー", + "xpack.metricsData.assetDetails.formulas.diskIORead": "ディスク読み取りIOPS", + "xpack.metricsData.assetDetails.formulas.diskIOWrite": "ディスク書き込みIOPS", + "xpack.metricsData.assetDetails.formulas.diskReadThroughput": "ディスク読み取りスループット", + "xpack.metricsData.assetDetails.formulas.diskSpaceAvailability": "空きディスク容量", + "xpack.metricsData.assetDetails.formulas.diskSpaceAvailable": "空きディスク容量", + "xpack.metricsData.assetDetails.formulas.diskUsage": "ディスク使用量", + "xpack.metricsData.assetDetails.formulas.diskWriteThroughput": "ディスク書き込みスループット", + "xpack.metricsData.assetDetails.formulas.hostCount.hostsLabel": "ホスト", + "xpack.metricsData.assetDetails.formulas.kubernetes.capacity": "容量", + "xpack.metricsData.assetDetails.formulas.kubernetes.used": "使用中", + "xpack.metricsData.assetDetails.formulas.load15m": "読み込み(15m)", + "xpack.metricsData.assetDetails.formulas.load1m": "読み込み(1m)", + "xpack.metricsData.assetDetails.formulas.load5m": "読み込み(5m)", + "xpack.metricsData.assetDetails.formulas.logRate": "ログレート", + "xpack.metricsData.assetDetails.formulas.memoryFree": "空きメモリー", + "xpack.metricsData.assetDetails.formulas.memoryUsage": "メモリー使用状況", + "xpack.metricsData.assetDetails.formulas.metric.label.cache": "キャッシュ", + "xpack.metricsData.assetDetails.formulas.metric.label.free": "空き", + "xpack.metricsData.assetDetails.formulas.metric.label.used": "使用中", + "xpack.metricsData.assetDetails.formulas.normalizedLoad1m": "正規化された負荷", + "xpack.metricsData.assetDetails.formulas.rx": "ネットワーク受信(RX)", + "xpack.metricsData.assetDetails.formulas.tx": "ネットワーク送信(TX)", + "xpack.metricsData.assetDetails.metricsCharts.diskIOPS": "Disk IOPS", + "xpack.metricsData.assetDetails.metricsCharts.diskThroughput": "Disk Throughput", + "xpack.metricsData.assetDetails.metricsCharts.diskUsage": "ディスク使用量", + "xpack.metricsData.assetDetails.metricsCharts.diskUsage.label.available": "利用可能", + "xpack.metricsData.assetDetails.metricsCharts.diskUsage.label.used": "使用中", + "xpack.metricsData.assetDetails.metricsCharts.diskUsageByMountingPoint": "マウントポイント別ディスク使用量", + "xpack.metricsData.assetDetails.metricsCharts.kubernetes.nodeCpuCapacity": "ノード CPU 処理能力", + "xpack.metricsData.assetDetails.metricsCharts.load": "読み込み", + "xpack.metricsData.assetDetails.metricsCharts.metric.label.cache": "キャッシュ", + "xpack.metricsData.assetDetails.metricsCharts.metric.label.free": "空き", + "xpack.metricsData.assetDetails.metricsCharts.metric.label.read": "読み取り", + "xpack.metricsData.assetDetails.metricsCharts.metric.label.used": "使用中", + "xpack.metricsData.assetDetails.metricsCharts.metric.label.write": "書き込み", + "xpack.metricsData.assetDetails.metricsCharts.network": "ネットワーク", + "xpack.metricsData.assetDetails.metricsCharts.network.label.rx": "受信(RX)", + "xpack.metricsData.assetDetails.metricsCharts.network.label.tx": "送信(TX)", + "xpack.metricsData.assetDetails.metricsCharts.kubernetes.nodeDiskCapacity": "ノードディスク容量", + "xpack.metricsData.assetDetails.metricsCharts.kubernetes.nodeMemoryCapacity": "ノードメモリー容量", + "xpack.metricsData.assetDetails.metricsCharts.kubernetes.nodePodCapacity": "ノードポッド容量", + "xpack.metricsData.assetDetails.overview.kpi.subtitle.average": "平均", "xpack.ingestPipelines.app.deniedPrivilegeDescription": "インジェストパイプラインを使用するには、{privilegesCount, plural, other {これらのクラスター権限}}が必要です:{missingPrivileges}。", "xpack.ingestPipelines.clone.loadSourcePipelineErrorTitle": "{name}を読み込めません。", "xpack.ingestPipelines.createFromCsv.errorMessage": "{message}", @@ -29286,46 +29378,6 @@ "xpack.observabilityAiAssistant.setupKb": "ナレッジベースを設定することで、エクスペリエンスが改善されます。", "xpack.observabilityAiAssistant.stopGeneratingButtonLabel": "生成を停止", "xpack.observabilityAiAssistant.technicalPreviewBadgeDescription": "関数呼び出し(根本原因分析やデータの視覚化など)を使用する際に、より一貫性のあるエクスペリエンスを実現するために、GPT4が必要です。GPT3.5は、エラーの説明などのシンプルなワークフローの一部や、頻繁な関数呼び出しの使用が必要とされないKibana内のエクスペリエンスなどのChatGPTで機能します。", - "xpack.observabilityShared.inspector.stats.queryTimeValue": "{queryTime}ms", - "xpack.observabilityShared.breadcrumbs.observabilityLinkText": "Observability", - "xpack.observabilityShared.inspector.stats.dataViewDescription": "Elasticsearchインデックスに接続したデータビューです。", - "xpack.observabilityShared.inspector.stats.dataViewLabel": "データビュー", - "xpack.observabilityShared.inspector.stats.hitsDescription": "クエリにより返されたドキュメントの数です。", - "xpack.observabilityShared.inspector.stats.hitsLabel": "ヒット数", - "xpack.observabilityShared.inspector.stats.hitsTotalDescription": "クエリに一致するドキュメントの数です。", - "xpack.observabilityShared.inspector.stats.hitsTotalLabel": "ヒット数(合計)", - "xpack.observabilityShared.inspector.stats.kibanaApiQueryParametersDescription": "Elasticsearch要求を開始したKibana API要求で使用されているクエリパラメーター。", - "xpack.observabilityShared.inspector.stats.kibanaApiQueryParametersLabel": "Kibana APIクエリパラメーター", - "xpack.observabilityShared.inspector.stats.kibanaApiRouteDescription": "Elasticsearch要求を開始したKibana API要求のルート。", - "xpack.observabilityShared.inspector.stats.kibanaApiRouteLabel": "Kibana APIルート", - "xpack.observabilityShared.inspector.stats.queryTimeDescription": "クエリの処理の所要時間です。リクエストの送信やブラウザーでのパースの時間は含まれません。", - "xpack.observabilityShared.inspector.stats.queryTimeLabel": "クエリ時間", - "xpack.observabilityShared.navigation.betaBadge": "ベータ", - "xpack.observabilityShared.navigation.experimentalBadgeLabel": "テクニカルプレビュー", - "xpack.observabilityShared.navigation.newBadge": "新規", - "xpack.observabilityShared.pageLayout.sideNavTitle": "Observability", - "xpack.observabilityShared.sectionLink.newLabel": "新規", - "xpack.observabilityShared.technicalPreviewBadgeDescription": "この機能はテクニカルプレビュー中であり、将来のリリースでは変更されたり完全に削除されたりする場合があります。Elasticは最善の努力を講じてすべての問題の修正に努めますが、テクニカルプレビュー中の機能には正式なGA機能のサポートSLAが適用されません。", - "xpack.observabilityShared.technicalPreviewBadgeLabel": "テクニカルプレビュー", - "xpack.observabilityShared.tour.alertsStep.imageAltText": "アラートデモ", - "xpack.observabilityShared.tour.alertsStep.tourContent": "電子メール、PagerDuty、Slackなどのサードパーティプラットフォーム統合でアラートをトリガーする条件を定義して検出します。", - "xpack.observabilityShared.tour.alertsStep.tourTitle": "変更が発生したときに通知", - "xpack.observabilityShared.tour.endButtonLabel": "ツアーを終了", - "xpack.observabilityShared.tour.guidedSetupStep.tourContent": "Elasticオブザーバビリティに進む最も簡単な方法は、データアシスタントで推奨された次のステップに従うことです。", - "xpack.observabilityShared.tour.guidedSetupStep.tourTitle": "Elasticオブザーバビリティのその他の機能", - "xpack.observabilityShared.tour.metricsExplorerStep.imageAltText": "メトリックエクスプローラーのデモ", - "xpack.observabilityShared.tour.metricsExplorerStep.tourContent": "システム、クラウド、ネットワーク、その他のインフラストラクチャーソースからメトリックをストリーム、グループ化、可視化します。", - "xpack.observabilityShared.tour.metricsExplorerStep.tourTitle": "インフラストラクチャーの正常性を監視", - "xpack.observabilityShared.tour.nextButtonLabel": "次へ", - "xpack.observabilityShared.tour.observabilityOverviewStep.tourContent": "クイックガイドを表示し、オブザーバビリティデータすべてを1つのスタックに格納する利点をご覧ください。", - "xpack.observabilityShared.tour.observabilityOverviewStep.tourTitle": "Elasticオブザーバビリティへようこそ", - "xpack.observabilityShared.tour.servicesStep.imageAltText": "サービスのデモ", - "xpack.observabilityShared.tour.servicesStep.tourContent": "サービスに関する詳細情報を収集し、パフォーマンスの問題をすばやく検出、修正できます。", - "xpack.observabilityShared.tour.servicesStep.tourTitle": "アプリケーションの問題を特定して解決", - "xpack.observabilityShared.tour.skipButtonLabel": "ツアーをスキップ", - "xpack.observabilityShared.tour.streamStep.imageAltText": "ログストリームのデモ", - "xpack.observabilityShared.tour.streamStep.tourContent": "アプリケーション、サーバー、仮想マシン、コネクターからのログイベントを監視、フィルター、検査します。", - "xpack.observabilityShared.tour.streamStep.tourTitle": "リアルタイムでログを追跡", "xpack.osquery.action.missingPrivileges": "このページにアクセスするには、{osquery} Kibana権限について管理者に確認してください。", "xpack.osquery.agentPolicy.confirmModalCalloutDescription": "選択した{agentPolicyCount, plural, other {エージェントポリシー}}が一部のエージェントですでに使用されていることをFleetが検出しました。このアクションの結果として、Fleetはこの{agentPolicyCount, plural, other {エージェントポリシー}}を使用しているすべてのエージェントに更新をデプロイします。", "xpack.osquery.agentPolicy.confirmModalCalloutTitle": "{agentCount, plural, other {#個のエージェント}}が更新されます", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a0f4b527e06718..1854713002cfdd 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8044,7 +8044,6 @@ "xpack.apm.anomalyDetection.createJobs.failed.text": "为 APM 服务环境 [{environments}] 创建一个或多个异常检测作业时出现问题。错误:“{errorMessage}”", "xpack.apm.anomalyDetection.createJobs.succeeded.text": "APM 服务环境 [{environments}] 的异常检测作业已成功创建。Machine Learning 要过一些时间才会开始分析流量以发现异常。", "xpack.apm.anomalyDetectionSetup.notEnabledForEnvironmentText": "尚未针对环境“{currentEnvironment}”启用异常检测。单击可继续设置。", - "xpack.apm.bottomBarActions.unsavedChanges": "{unsavedChangesCount, plural, =0 {0 个未保存更改} other {# 个未保存更改}} ", "xpack.apm.compositeSpanCallsLabel": ",{count} 个调用,平均 {duration}", "xpack.apm.correlations.ccsWarningCalloutBody": "无法完全检索相关性分析的数据。仅 {version} 及更高版本支持此功能。", "xpack.apm.correlations.failedTransactions.helpPopover.basicExplanation": "相关性将帮助您发现哪些属性在区分事务失败与成功时具有最大影响。如果事务的 {field} 值为 {value},则认为其失败。", @@ -8468,7 +8467,6 @@ "xpack.apm.appName": "APM", "xpack.apm.betaBadgeDescription": "此功能当前为公测版。如果遇到任何错误或有任何反馈,请报告问题或访问我们的论坛。", "xpack.apm.betaBadgeLabel": "公测版", - "xpack.apm.bottomBarActions.discardChangesButton": "放弃更改", "xpack.apm.chart.annotation.version": "版本", "xpack.apm.chart.comparison.defaultPreviousPeriodLabel": "上一时段", "xpack.apm.chart.cpuSeries.processAverageLabel": "进程平均值", @@ -20790,6 +20788,7 @@ "xpack.infra.sourceConfiguration.noRemoteClusterTitle": "无法连接到远程集群", "xpack.infra.sourceConfiguration.remoteClusterConnectionDoNotExist": "检查远程集群是否可用,或远程连接设置是否正确。", "xpack.infra.sourceConfiguration.remoteClusterConnectionDoNotExistTitle": "无法连接到远程集群", + "xpack.infra.sourceConfiguration.saveButton": "保存更改", "xpack.infra.sourceConfiguration.systemColumnBadgeLabel": "系统", "xpack.infra.sourceConfiguration.unsavedFormPrompt": "是否确定要离开?更改将丢失", "xpack.infra.sourceConfiguration.updateFailureBody": "无法对指标配置应用更改。请稍后重试。", @@ -20893,6 +20892,98 @@ "xpack.infra.waffle.unableToSelectMetricErrorTitle": "无法选择指标选项或指标值。", "xpack.infra.waffleTime.autoRefreshButtonLabel": "自动刷新", "xpack.infra.waffleTime.stopRefreshingButtonLabel": "停止刷新", + "xpack.observabilityShared.featureFeedbackButton.tellUsWhatYouThinkLink": "告诉我们您的看法!", + "xpack.observabilityShared.bottomBarActions.discardChangesButton": "放弃更改", + "xpack.observabilityShared.bottomBarActions.unsavedChanges": "{unsavedChangesCount, plural, =0 {0 个未保存更改} other {# 个未保存更改}} ", + "xpack.observabilityShared.breadcrumbs.observabilityLinkText": "Observability", + "xpack.observabilityShared.inspector.stats.queryTimeValue": "{queryTime}ms", + "xpack.observabilityShared.inspector.stats.dataViewDescription": "连接到 Elasticsearch 索引的数据视图。", + "xpack.observabilityShared.inspector.stats.dataViewLabel": "数据视图", + "xpack.observabilityShared.inspector.stats.hitsDescription": "查询返回的文档数目。", + "xpack.observabilityShared.inspector.stats.hitsLabel": "命中数", + "xpack.observabilityShared.inspector.stats.hitsTotalDescription": "与查询匹配的文档数目。", + "xpack.observabilityShared.inspector.stats.hitsTotalLabel": "命中数(总数)", + "xpack.observabilityShared.inspector.stats.kibanaApiQueryParametersDescription": "发起 Elasticsearch 请求的 Kibana API 请求中使用的查询参数。", + "xpack.observabilityShared.inspector.stats.kibanaApiQueryParametersLabel": "Kibana API 查询参数", + "xpack.observabilityShared.inspector.stats.kibanaApiRouteDescription": "发起 Elasticsearch 请求的 Kibana API 请求的路由。", + "xpack.observabilityShared.inspector.stats.kibanaApiRouteLabel": "Kibana API 路由", + "xpack.observabilityShared.inspector.stats.queryTimeDescription": "处理查询所花费的时间。不包括发送请求或在浏览器中解析它的时间。", + "xpack.observabilityShared.inspector.stats.queryTimeLabel": "查询时间", + "xpack.observabilityShared.navigation.betaBadge": "公测版", + "xpack.observabilityShared.navigation.experimentalBadgeLabel": "技术预览", + "xpack.observabilityShared.navigation.newBadge": "新建", + "xpack.observabilityShared.pageLayout.sideNavTitle": "Observability", + "xpack.observabilityShared.sectionLink.newLabel": "新建", + "xpack.observabilityShared.technicalPreviewBadgeDescription": "此功能处于技术预览状态,在未来版本中可能会更改或完全移除。Elastic 将尽最大努力来修复任何问题,但处于技术预览状态的功能不受正式 GA 功能支持 SLA 的约束。", + "xpack.observabilityShared.technicalPreviewBadgeLabel": "技术预览", + "xpack.observabilityShared.tour.alertsStep.imageAltText": "告警演示", + "xpack.observabilityShared.tour.alertsStep.tourContent": "通过电子邮件、PagerDuty 和 Slack 等第三方平台集成定义并检测触发告警的条件。", + "xpack.observabilityShared.tour.alertsStep.tourTitle": "发生更改时接收通知", + "xpack.observabilityShared.tour.endButtonLabel": "结束教程", + "xpack.observabilityShared.tour.guidedSetupStep.tourContent": "继续使用 Elastic Observability 的最简便方法,是按照数据助手中推荐的后续步骤操作。", + "xpack.observabilityShared.tour.guidedSetupStep.tourTitle": "Elastic Observability 让您事半功倍", + "xpack.observabilityShared.tour.metricsExplorerStep.imageAltText": "指标浏览器演示", + "xpack.observabilityShared.tour.metricsExplorerStep.tourContent": "流式传输、分组并可视化您的系统、云、网络和其他基础架构源中的指标。", + "xpack.observabilityShared.tour.metricsExplorerStep.tourTitle": "监测基础架构运行状况", + "xpack.observabilityShared.tour.nextButtonLabel": "下一步", + "xpack.observabilityShared.tour.observabilityOverviewStep.tourContent": "学习快速教程以了解在一个堆栈中保存所有 Observability 数据的优势。", + "xpack.observabilityShared.tour.observabilityOverviewStep.tourTitle": "欢迎使用 Elastic Observability", + "xpack.observabilityShared.tour.servicesStep.imageAltText": "服务演示", + "xpack.observabilityShared.tour.servicesStep.tourContent": "通过收集有关服务的详细信息快速查找并修复性能问题。", + "xpack.observabilityShared.tour.servicesStep.tourTitle": "确定并解决应用程序问题", + "xpack.observabilityShared.tour.skipButtonLabel": "跳过教程", + "xpack.observabilityShared.tour.streamStep.imageAltText": "日志流演示", + "xpack.observabilityShared.tour.streamStep.tourContent": "监测、筛选并检查从您的应用程序、服务器、虚拟机和容器中流入的日志事件。", + "xpack.observabilityShared.tour.streamStep.tourTitle": "实时跟踪您的日志", + "xpack.metricsData.assetDetails.formulas.cpuUsage": "CPU 使用率", + "xpack.metricsData.assetDetails.formulas.cpuUsage.iowaitLabel": "iowait", + "xpack.metricsData.assetDetails.formulas.cpuUsage.irqLabel": "irq", + "xpack.metricsData.assetDetails.formulas.cpuUsage.niceLabel": "nice", + "xpack.metricsData.assetDetails.formulas.cpuUsage.softirqLabel": "softirq", + "xpack.metricsData.assetDetails.formulas.cpuUsage.stealLabel": "steal", + "xpack.metricsData.assetDetails.formulas.cpuUsage.systemLabel": "system", + "xpack.metricsData.assetDetails.formulas.cpuUsage.userLabel": "用户", + "xpack.metricsData.assetDetails.formulas.diskIORead": "磁盘读取 IOPS", + "xpack.metricsData.assetDetails.formulas.diskIOWrite": "磁盘写入 IOPS", + "xpack.metricsData.assetDetails.formulas.diskReadThroughput": "磁盘读取吞吐量", + "xpack.metricsData.assetDetails.formulas.diskSpaceAvailability": "磁盘空间可用性", + "xpack.metricsData.assetDetails.formulas.diskSpaceAvailable": "可用磁盘空间", + "xpack.metricsData.assetDetails.formulas.diskUsage": "磁盘使用率", + "xpack.metricsData.assetDetails.formulas.diskWriteThroughput": "磁盘写入吞吐量", + "xpack.metricsData.assetDetails.formulas.hostCount.hostsLabel": "主机", + "xpack.metricsData.assetDetails.formulas.kubernetes.capacity": "容量", + "xpack.metricsData.assetDetails.formulas.kubernetes.used": "已使用", + "xpack.metricsData.assetDetails.formulas.load15m": "负载(15 分钟)", + "xpack.metricsData.assetDetails.formulas.load1m": "负载(1 分钟)", + "xpack.metricsData.assetDetails.formulas.load5m": "负载(5 分钟)", + "xpack.metricsData.assetDetails.formulas.logRate": "日志速率", + "xpack.metricsData.assetDetails.formulas.memoryFree": "可用内存", + "xpack.metricsData.assetDetails.formulas.memoryUsage": "内存利用率", + "xpack.metricsData.assetDetails.formulas.metric.label.cache": "缓存", + "xpack.metricsData.assetDetails.formulas.metric.label.free": "可用", + "xpack.metricsData.assetDetails.formulas.metric.label.used": "已使用", + "xpack.metricsData.assetDetails.formulas.normalizedLoad1m": "标准化负载", + "xpack.metricsData.assetDetails.formulas.rx": "网络入站数据 (RX)", + "xpack.metricsData.assetDetails.formulas.tx": "网络出站数据 (TX)", + "xpack.metricsData.assetDetails.metricsCharts.diskIOPS": "磁盘 IOPS", + "xpack.metricsData.assetDetails.metricsCharts.diskThroughput": "磁盘吞吐量", + "xpack.metricsData.assetDetails.metricsCharts.diskUsage.label.available": "可用", + "xpack.metricsData.assetDetails.metricsCharts.diskUsage.label.used": "已使用", + "xpack.metricsData.assetDetails.metricsCharts.diskUsageByMountingPoint": "磁盘使用率(按装载点)", + "xpack.metricsData.assetDetails.metricsCharts.kubernetes.nodeCpuCapacity": "节点 CPU 容量", + "xpack.metricsData.assetDetails.metricsCharts.load": "加载", + "xpack.metricsData.assetDetails.metricsCharts.metric.label.cache": "缓存", + "xpack.metricsData.assetDetails.metricsCharts.metric.label.free": "可用", + "xpack.metricsData.assetDetails.metricsCharts.metric.label.read": "读取", + "xpack.metricsData.assetDetails.metricsCharts.metric.label.used": "已使用", + "xpack.metricsData.assetDetails.metricsCharts.metric.label.write": "写入", + "xpack.metricsData.assetDetails.metricsCharts.network": "网络", + "xpack.metricsData.assetDetails.metricsCharts.network.label.rx": "入站 (RX)", + "xpack.metricsData.assetDetails.metricsCharts.network.label.tx": "出站 (TX)", + "xpack.metricsData.assetDetails.metricsCharts.kubernetes.nodeDiskCapacity": "节点磁盘容量", + "xpack.metricsData.assetDetails.metricsCharts.kubernetes.nodeMemoryCapacity": "节点内存容量", + "xpack.metricsData.assetDetails.metricsCharts.kubernetes.nodePodCapacity": "节点 Pod 容量", + "xpack.metricsData.assetDetails.overview.kpi.subtitle.average": "平均值", "xpack.ingestPipelines.app.deniedPrivilegeDescription": "要使用采集管道,您必须具有{privilegesCount, plural, other {以下集群权限}}:{missingPrivileges}。", "xpack.ingestPipelines.clone.loadSourcePipelineErrorTitle": "无法加载 {name}。", "xpack.ingestPipelines.createFromCsv.errorMessage": "{message}", @@ -29271,46 +29362,6 @@ "xpack.observabilityAiAssistant.setupKb": "通过设置知识库来改进体验。", "xpack.observabilityAiAssistant.stopGeneratingButtonLabel": "停止生成", "xpack.observabilityAiAssistant.technicalPreviewBadgeDescription": "需要 GPT4 以在使用函数调用时(例如,执行根本原因分析、数据可视化等时候)获得更加一致的体验。GPT3.5 可作用于某些更简单的工作流(如解释错误),或在 Kibana 中获得不需要频繁使用函数调用的与 ChatGPT 类似的体验。", - "xpack.observabilityShared.inspector.stats.queryTimeValue": "{queryTime}ms", - "xpack.observabilityShared.breadcrumbs.observabilityLinkText": "Observability", - "xpack.observabilityShared.inspector.stats.dataViewDescription": "连接到 Elasticsearch 索引的数据视图。", - "xpack.observabilityShared.inspector.stats.dataViewLabel": "数据视图", - "xpack.observabilityShared.inspector.stats.hitsDescription": "查询返回的文档数目。", - "xpack.observabilityShared.inspector.stats.hitsLabel": "命中数", - "xpack.observabilityShared.inspector.stats.hitsTotalDescription": "与查询匹配的文档数目。", - "xpack.observabilityShared.inspector.stats.hitsTotalLabel": "命中数(总数)", - "xpack.observabilityShared.inspector.stats.kibanaApiQueryParametersDescription": "发起 Elasticsearch 请求的 Kibana API 请求中使用的查询参数。", - "xpack.observabilityShared.inspector.stats.kibanaApiQueryParametersLabel": "Kibana API 查询参数", - "xpack.observabilityShared.inspector.stats.kibanaApiRouteDescription": "发起 Elasticsearch 请求的 Kibana API 请求的路由。", - "xpack.observabilityShared.inspector.stats.kibanaApiRouteLabel": "Kibana API 路由", - "xpack.observabilityShared.inspector.stats.queryTimeDescription": "处理查询所花费的时间。不包括发送请求或在浏览器中解析它的时间。", - "xpack.observabilityShared.inspector.stats.queryTimeLabel": "查询时间", - "xpack.observabilityShared.navigation.betaBadge": "公测版", - "xpack.observabilityShared.navigation.experimentalBadgeLabel": "技术预览", - "xpack.observabilityShared.navigation.newBadge": "新建", - "xpack.observabilityShared.pageLayout.sideNavTitle": "Observability", - "xpack.observabilityShared.sectionLink.newLabel": "新建", - "xpack.observabilityShared.technicalPreviewBadgeDescription": "此功能处于技术预览状态,在未来版本中可能会更改或完全移除。Elastic 将尽最大努力来修复任何问题,但处于技术预览状态的功能不受正式 GA 功能支持 SLA 的约束。", - "xpack.observabilityShared.technicalPreviewBadgeLabel": "技术预览", - "xpack.observabilityShared.tour.alertsStep.imageAltText": "告警演示", - "xpack.observabilityShared.tour.alertsStep.tourContent": "通过电子邮件、PagerDuty 和 Slack 等第三方平台集成定义并检测触发告警的条件。", - "xpack.observabilityShared.tour.alertsStep.tourTitle": "发生更改时接收通知", - "xpack.observabilityShared.tour.endButtonLabel": "结束教程", - "xpack.observabilityShared.tour.guidedSetupStep.tourContent": "继续使用 Elastic Observability 的最简便方法,是按照数据助手中推荐的后续步骤操作。", - "xpack.observabilityShared.tour.guidedSetupStep.tourTitle": "Elastic Observability 让您事半功倍", - "xpack.observabilityShared.tour.metricsExplorerStep.imageAltText": "指标浏览器演示", - "xpack.observabilityShared.tour.metricsExplorerStep.tourContent": "流式传输、分组并可视化您的系统、云、网络和其他基础架构源中的指标。", - "xpack.observabilityShared.tour.metricsExplorerStep.tourTitle": "监测基础架构运行状况", - "xpack.observabilityShared.tour.nextButtonLabel": "下一步", - "xpack.observabilityShared.tour.observabilityOverviewStep.tourContent": "学习快速教程以了解在一个堆栈中保存所有 Observability 数据的优势。", - "xpack.observabilityShared.tour.observabilityOverviewStep.tourTitle": "欢迎使用 Elastic Observability", - "xpack.observabilityShared.tour.servicesStep.imageAltText": "服务演示", - "xpack.observabilityShared.tour.servicesStep.tourContent": "通过收集有关服务的详细信息快速查找并修复性能问题。", - "xpack.observabilityShared.tour.servicesStep.tourTitle": "确定并解决应用程序问题", - "xpack.observabilityShared.tour.skipButtonLabel": "跳过教程", - "xpack.observabilityShared.tour.streamStep.imageAltText": "日志流演示", - "xpack.observabilityShared.tour.streamStep.tourContent": "监测、筛选并检查从您的应用程序、服务器、虚拟机和容器中流入的日志事件。", - "xpack.observabilityShared.tour.streamStep.tourTitle": "实时跟踪您的日志", "xpack.osquery.action.missingPrivileges": "要访问此页面,请联系管理员获取 {osquery} Kibana 权限。", "xpack.osquery.agentPolicy.confirmModalCalloutDescription": "Fleet 检测到您的部分代理已在使用选定{agentPolicyCount, plural, other {代理策略}}。由于此操作,Fleet 会将更新部署到使用此{agentPolicyCount, plural, other {代理策略}}的所有代理。", "xpack.osquery.agentPolicy.confirmModalCalloutTitle": "此操作将更新 {agentCount, plural, other {# 个代理}}", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx index a2f8b5a2780d22..5458f03ebeaadc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx @@ -79,7 +79,8 @@ export const TestExpression: FunctionComponent = () => { ); }; -describe('rule_add', () => { +// FLAKY: https://github.com/elastic/kibana/issues/174397 +describe.skip('rule_add', () => { afterEach(() => { jest.clearAllMocks(); }); diff --git a/x-pack/test/api_integration/apis/file_upload/index.ts b/x-pack/test/api_integration/apis/file_upload/index.ts index 30fd1ae819598d..f2232c0cfcbce3 100644 --- a/x-pack/test/api_integration/apis/file_upload/index.ts +++ b/x-pack/test/api_integration/apis/file_upload/index.ts @@ -11,5 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('File upload', function () { loadTestFile(require.resolve('./has_import_permission')); loadTestFile(require.resolve('./index_exists')); + loadTestFile(require.resolve('./preview_index_time_range')); }); } diff --git a/x-pack/test/api_integration/apis/file_upload/preview_index_time_range.ts b/x-pack/test/api_integration/apis/file_upload/preview_index_time_range.ts new file mode 100644 index 00000000000000..bd980efd94ecd4 --- /dev/null +++ b/x-pack/test/api_integration/apis/file_upload/preview_index_time_range.ts @@ -0,0 +1,353 @@ +/* + * 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 { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + + const fqPipeline = { + description: 'Ingest pipeline created by text structure finder', + processors: [ + { + csv: { + field: 'message', + target_fields: ['time', 'airline', 'responsetime', 'sourcetype'], + ignore_missing: false, + }, + }, + { + date: { + field: 'time', + formats: ['yyyy-MM-dd HH:mm:ssXX'], + }, + }, + { + convert: { + field: 'responsetime', + type: 'double', + ignore_missing: true, + }, + }, + { + remove: { + field: 'message', + }, + }, + ], + }; + const fqTimeField = '@timestamp'; + + async function runRequest(docs: any[], pipeline: any, timeField: string) { + const { body } = await supertest + .post(`/internal/file_upload/preview_index_time_range`) + .set('kbn-xsrf', 'kibana') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .send({ docs, pipeline, timeField }) + .expect(200); + + return body; + } + + describe('POST /internal/file_upload/preview_index_time_range', () => { + it('should return the correct start and end for normal data', async () => { + const resp = await runRequest( + [ + { + message: '2014-06-20 00:00:00Z,AAL,132.2046,farequote', + }, + { + message: '2014-06-21 00:00:00Z,JZA,990.4628,farequote', + }, + { + message: '2014-06-22 00:00:00Z,JBU,877.5927,farequote', + }, + { + message: '2014-06-23 00:00:00Z,KLM,1355.4812,farequote', + }, + { + message: '2014-06-24 00:00:00Z,NKS,9991.3981,farequote', + }, + { + message: '2014-06-26 23:59:35Z,JBU,923.6772,farequote', + }, + { + message: '2014-06-27 23:59:45Z,ACA,21.5385,farequote', + }, + { + message: '2014-06-28 23:59:54Z,FFT,251.573,farequote', + }, + { + message: '2014-06-29 23:59:54Z,ASA,78.2927,farequote', + }, + { + message: '2014-06-30 23:59:56Z,AWE,19.6438,farequote', + }, + ], + fqPipeline, + fqTimeField + ); + + expect(resp).to.eql({ + start: 1403222400000, + end: 1404172796000, + }); + }); + + it('should return the correct start and end for normal data out of order', async () => { + const resp = await runRequest( + [ + { + message: '2014-06-22 00:00:00Z,JBU,877.5927,farequote', + }, + { + message: '2014-06-21 00:00:00Z,JZA,990.4628,farequote', + }, + { + message: '2014-06-20 00:00:00Z,AAL,132.2046,farequote', + }, + { + message: '2014-06-23 00:00:00Z,KLM,1355.4812,farequote', + }, + { + message: '2014-06-24 00:00:00Z,NKS,9991.3981,farequote', + }, + { + message: '2014-06-26 23:59:35Z,JBU,923.6772,farequote', + }, + { + message: '2014-06-27 23:59:45Z,ACA,21.5385,farequote', + }, + { + message: '2014-06-30 23:59:56Z,AWE,19.6438,farequote', + }, + { + message: '2014-06-28 23:59:54Z,FFT,251.573,farequote', + }, + { + message: '2014-06-29 23:59:54Z,ASA,78.2927,farequote', + }, + ], + fqPipeline, + fqTimeField + ); + + expect(resp).to.eql({ + start: 1403222400000, + end: 1404172796000, + }); + }); + + it('should return the correct start and end for data with bad last doc', async () => { + const resp = await runRequest( + [ + { + message: '2014-06-20 00:00:00Z,AAL,132.2046,farequote', + }, + { + message: '2014-06-21 00:00:00Z,JZA,990.4628,farequote', + }, + { + message: '2014-06-22 00:00:00Z,JBU,877.5927,farequote', + }, + { + message: '2014-06-23 00:00:00Z,KLM,1355.4812,farequote', + }, + { + message: '2014-06-24 00:00:00Z,NKS,9991.3981,farequote', + }, + { + message: '2014-06-26 23:59:35Z,JBU,923.6772,farequote', + }, + { + message: '2014-06-27 23:59:45Z,ACA,21.5385,farequote', + }, + { + message: '2014-06-28 23:59:54Z,FFT,251.573,farequote', + }, + { + message: '2014-06-29 23:59:54Z,ASA,78.2927,farequote', + }, + { + // bad data + message: '2014-06-bad 23:59:56Z,AWE,19.6438,farequote', + }, + ], + fqPipeline, + fqTimeField + ); + + expect(resp).to.eql({ + start: 1403222400000, + end: 1404086394000, + }); + }); + + it('should return the correct start and end for data with bad data near the end', async () => { + const resp = await runRequest( + [ + { + message: '2014-06-20 00:00:00Z,AAL,132.2046,farequote', + }, + { + message: '2014-06-21 00:00:00Z,JZA,990.4628,farequote', + }, + { + message: '2014-06-22 00:00:00Z,JBU,877.5927,farequote', + }, + { + message: '2014-06-23 00:00:00Z,KLM,1355.4812,farequote', + }, + { + message: '2014-06-24 00:00:00Z,NKS,9991.3981,farequote', + }, + { + message: '2014-06-26 23:59:35Z,JBU,923.6772,farequote', + }, + { + message: '2014-06-27 23:59:45Z,ACA,21.5385,farequote', + }, + { + message: '2014-06-28 23:59:54Z,FFT,251.573,farequote', + }, + { + // bad data + message: '2014-06-bad 23:59:54Z,ASA,78.2927,farequote', + }, + { + message: '2014-06-30 23:59:56Z,AWE,19.6438,farequote', + }, + ], + fqPipeline, + fqTimeField + ); + + expect(resp).to.eql({ + start: 1403222400000, + end: 1404172796000, + }); + }); + + it('should return the correct start and end for data with bad first doc', async () => { + const resp = await runRequest( + [ + { + // bad data + message: '2014-06-bad 00:00:00Z,AAL,132.2046,farequote', + }, + { + message: '2014-06-21 00:00:00Z,JZA,990.4628,farequote', + }, + { + message: '2014-06-22 00:00:00Z,JBU,877.5927,farequote', + }, + { + message: '2014-06-23 00:00:00Z,KLM,1355.4812,farequote', + }, + { + message: '2014-06-24 00:00:00Z,NKS,9991.3981,farequote', + }, + { + message: '2014-06-26 23:59:35Z,JBU,923.6772,farequote', + }, + { + message: '2014-06-27 23:59:45Z,ACA,21.5385,farequote', + }, + { + message: '2014-06-28 23:59:54Z,FFT,251.573,farequote', + }, + { + message: '2014-06-29 23:59:54Z,ASA,78.2927,farequote', + }, + { + message: '2014-06-30 23:59:56Z,AWE,19.6438,farequote', + }, + ], + fqPipeline, + fqTimeField + ); + + expect(resp).to.eql({ + start: 1403308800000, + end: 1404172796000, + }); + }); + + it('should return the correct start and end for data with bad near the start', async () => { + const resp = await runRequest( + [ + { + message: '2014-06-20 00:00:00Z,AAL,132.2046,farequote', + }, + { + // bad data + message: '2014-06-bad 00:00:00Z,JZA,990.4628,farequote', + }, + { + message: '2014-06-22 00:00:00Z,JBU,877.5927,farequote', + }, + { + message: '2014-06-23 00:00:00Z,KLM,1355.4812,farequote', + }, + { + message: '2014-06-24 00:00:00Z,NKS,9991.3981,farequote', + }, + { + message: '2014-06-26 23:59:35Z,JBU,923.6772,farequote', + }, + { + message: '2014-06-27 23:59:45Z,ACA,21.5385,farequote', + }, + { + message: '2014-06-28 23:59:54Z,FFT,251.573,farequote', + }, + { + message: '2014-06-29 23:59:54Z,ASA,78.2927,farequote', + }, + { + message: '2014-06-30 23:59:56Z,AWE,19.6438,farequote', + }, + ], + fqPipeline, + fqTimeField + ); + + expect(resp).to.eql({ + start: 1403222400000, + end: 1404172796000, + }); + }); + + it('should return null start and end for entire bad data', async () => { + const resp = await runRequest( + [ + { + message: '2014-06-bad 00:00:00Z,AAL,132.2046,farequote', + }, + { + message: '2014-06-bad 00:00:00Z,JZA,990.4628,farequote', + }, + { + message: '2014-06-bad 00:00:00Z,JBU,877.5927,farequote', + }, + { + message: '2014-06-bad 00:00:00Z,KLM,1355.4812,farequote', + }, + ], + fqPipeline, + fqTimeField + ); + + expect(resp).to.eql({ + start: null, + end: null, + }); + }); + }); +}; diff --git a/x-pack/test/functional/apps/infra/metrics_anomalies.ts b/x-pack/test/functional/apps/infra/metrics_anomalies.ts index bdda5aa0a3e9fd..a222fa4d7a0349 100644 --- a/x-pack/test/functional/apps/infra/metrics_anomalies.ts +++ b/x-pack/test/functional/apps/infra/metrics_anomalies.ts @@ -61,11 +61,18 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_anomalies'); }); it('renders the anomaly table with anomalies', async () => { + // if the input value is unchanged the save button won't be available + // after the change of the settings action added in + // https://github.com/elastic/kibana/pull/175024 + // to avoid the mentioned issue we can change the value and put it back to 50 + await pageObjects.infraHome.goToSettings(); + await pageObjects.infraHome.setAnomaliesThreshold('51'); + await infraSourceConfigurationForm.saveInfraSettings(); // default threshold should already be 50 but trying to prevent unknown flakiness by setting it // https://github.com/elastic/kibana/issues/100445 await pageObjects.infraHome.goToSettings(); await pageObjects.infraHome.setAnomaliesThreshold('50'); - await infraSourceConfigurationForm.saveConfiguration(); + await infraSourceConfigurationForm.saveInfraSettings(); await pageObjects.infraHome.goToInventory(); await pageObjects.infraHome.openAnomalyFlyout(); await pageObjects.infraHome.goToAnomaliesTab(); @@ -91,7 +98,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('renders more anomalies on threshold change', async () => { await pageObjects.infraHome.goToSettings(); await pageObjects.infraHome.setAnomaliesThreshold('25'); - await infraSourceConfigurationForm.saveConfiguration(); + await infraSourceConfigurationForm.saveInfraSettings(); await pageObjects.infraHome.goToInventory(); await pageObjects.infraHome.openAnomalyFlyout(); await pageObjects.infraHome.goToAnomaliesTab(); diff --git a/x-pack/test/functional/apps/infra/metrics_source_configuration.ts b/x-pack/test/functional/apps/infra/metrics_source_configuration.ts index c9673448fd8ddf..185e0e18fd7611 100644 --- a/x-pack/test/functional/apps/infra/metrics_source_configuration.ts +++ b/x-pack/test/functional/apps/infra/metrics_source_configuration.ts @@ -5,6 +5,7 @@ * 2.0. */ +import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; import { DATES } from './constants'; @@ -49,12 +50,31 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await metricIndicesInput.clearValueWithKeyboard({ charByChar: true }); await metricIndicesInput.type('does-not-exist-*'); - await infraSourceConfigurationForm.saveConfiguration(); + await infraSourceConfigurationForm.saveInfraSettings(); await pageObjects.infraHome.waitForLoading(); await pageObjects.infraHome.getInfraMissingMetricsIndicesCallout(); }); + it('can clear the input and reset to previous values without saving', async () => { + await pageObjects.common.navigateToUrlWithBrowserHistory('infraOps', '/settings'); + + const nameInput = await infraSourceConfigurationForm.getNameInput(); + const previousNameInputText = await nameInput.getAttribute('value'); + await nameInput.clearValueWithKeyboard({ charByChar: true }); + await nameInput.type('New Source'); + + const metricIndicesInput = await infraSourceConfigurationForm.getMetricIndicesInput(); + await metricIndicesInput.clearValueWithKeyboard({ charByChar: true }); + await metricIndicesInput.type('this-is-new-change-*'); + + await infraSourceConfigurationForm.discardInfraSettingsChanges(); + + // Check for previous value + const nameInputText = await nameInput.getAttribute('value'); + expect(nameInputText).to.equal(previousNameInputText); + }); + it('renders the no indices screen when no indices match the pattern', async () => { await pageObjects.common.navigateToApp('infraOps'); await pageObjects.infraHome.getNoMetricsIndicesPrompt(); @@ -71,7 +91,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await metricIndicesInput.clearValueWithKeyboard({ charByChar: true }); await metricIndicesInput.type('remote_cluster:metricbeat-*'); - await infraSourceConfigurationForm.saveConfiguration(); + await infraSourceConfigurationForm.saveInfraSettings(); await pageObjects.infraHome.waitForLoading(); await pageObjects.infraHome.getInfraMissingRemoteClusterIndicesCallout(); @@ -89,7 +109,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await metricIndicesInput.clearValueWithKeyboard({ charByChar: true }); await metricIndicesInput.type('metricbeat-*'); - await infraSourceConfigurationForm.saveConfiguration(); + await infraSourceConfigurationForm.saveInfraSettings(); }); it('renders the waffle map again', async () => { diff --git a/x-pack/test/functional/fixtures/kbn_archiver/saved_objects_management/visible_in_management.json b/x-pack/test/functional/fixtures/kbn_archiver/saved_objects_management/visible_in_management.json new file mode 100644 index 00000000000000..9d4a88fe9c1fd7 --- /dev/null +++ b/x-pack/test/functional/fixtures/kbn_archiver/saved_objects_management/visible_in_management.json @@ -0,0 +1,12 @@ +{ + "attributes": { + "enabled": true, + "title": "vim-1" + }, + "coreMigrationVersion": "7.14.0", + "id": "test-not-visible-in-management:vim-1", + "references": [], + "type": "test-not-visible-in-management", + "updated_at": "2018-12-21T00:43:07.096Z", + "version": "WzIsMV0=" +} diff --git a/x-pack/test/functional/services/infra_source_configuration_form.ts b/x-pack/test/functional/services/infra_source_configuration_form.ts index 805dfcbbc9dcb4..da39347c363899 100644 --- a/x-pack/test/functional/services/infra_source_configuration_form.ts +++ b/x-pack/test/functional/services/infra_source_configuration_form.ts @@ -109,6 +109,26 @@ export function InfraSourceConfigurationFormProvider({ await common.sleep(KEY_PRESS_DELAY_MS); }, + /** + * Infra Metrics bottom actions bar + */ + async getSaveButton(): Promise { + return await testSubjects.find('infraBottomBarActionsButton'); + }, + + async saveInfraSettings() { + await (await this.getSaveButton()).click(); + + await retry.try(async () => { + const element = await this.getSaveButton(); + return !(await element.isDisplayed()); + }); + }, + + async discardInfraSettingsChanges() { + await (await testSubjects.find('infraBottomBarActionsDiscardChangesButton')).click(); + }, + /** * Form */ diff --git a/x-pack/test/functional_execution_context/tests/browser.ts b/x-pack/test/functional_execution_context/tests/browser.ts index f7d265316decb8..63f575b798b93f 100644 --- a/x-pack/test/functional_execution_context/tests/browser.ts +++ b/x-pack/test/functional_execution_context/tests/browser.ts @@ -12,7 +12,8 @@ import { assertLogContains, isExecutionContextLog, readLogFile } from '../test_u export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'home', 'timePicker']); - describe('Browser apps', () => { + // FLAKY: https://github.com/elastic/kibana/issues/112103 + describe.skip('Browser apps', () => { let logs: Ecs[]; const retry = getService('retry'); diff --git a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/bulk_get.ts b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/bulk_get.ts new file mode 100644 index 00000000000000..78d4f41033388e --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/bulk_get.ts @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { Response } from 'supertest'; +import type { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['common', 'svlCommonPage', 'savedObjects']); + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const svlCommonApi = getService('svlCommonApi'); + const testSubjects = getService('testSubjects'); + + describe('_bulk_get', () => { + describe('saved objects with hidden type', () => { + before(async () => { + await esArchiver.load( + 'test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects' + ); + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/saved_objects_management/hidden_saved_objects' + ); + await pageObjects.svlCommonPage.login(); + await pageObjects.common.navigateToApp('management'); + await testSubjects.click('app-card-objects'); + await pageObjects.savedObjects.waitTableIsLoaded(); + }); + + after(async () => { + await esArchiver.unload( + 'test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects' + ); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/saved_objects_management/hidden_saved_objects' + ); + await kibanaServer.savedObjects.cleanStandardList(); + await pageObjects.svlCommonPage.forceLogout(); + }); + + const URL = '/api/kibana/management/saved_objects/_bulk_get'; + const hiddenTypeExportableImportable = { + type: 'test-hidden-importable-exportable', + id: 'ff3733a0-9fty-11e7-ahb3-3dcb94193fab', + }; + const hiddenTypeNonExportableImportable = { + type: 'test-hidden-non-importable-exportable', + id: 'op3767a1-9rcg-53u7-jkb3-3dnb74193awc', + }; + + function expectSuccess(index: number, { body }: Response) { + const { type, id, meta, error } = body[index]; + expect(type).to.eql(hiddenTypeExportableImportable.type); + expect(id).to.eql(hiddenTypeExportableImportable.id); + expect(meta).to.not.equal(undefined); + expect(error).to.equal(undefined); + } + + function expectBadRequest(index: number, { body }: Response) { + const { type, id, error } = body[index]; + expect(type).to.eql(hiddenTypeNonExportableImportable.type); + expect(id).to.eql(hiddenTypeNonExportableImportable.id); + expect(error).to.eql({ + message: `Unsupported saved object type: '${hiddenTypeNonExportableImportable.type}': Bad Request`, + statusCode: 400, + error: 'Bad Request', + }); + } + + it('should return 200 for hidden types that are importableAndExportable', async () => + await supertest + .post(URL) + .send([hiddenTypeExportableImportable]) + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .expect(200) + .then((response: Response) => { + expect(response.body).to.have.length(1); + expectSuccess(0, response); + })); + + it('should return error for hidden types that are not importableAndExportable', async () => + await supertest + .post(URL) + .send([hiddenTypeNonExportableImportable]) + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .expect(200) + .then((response: Response) => { + expect(response.body).to.have.length(1); + expectBadRequest(0, response); + })); + + it('should return mix of successes and errors', async () => + await supertest + .post(URL) + .send([hiddenTypeExportableImportable, hiddenTypeNonExportableImportable]) + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .expect(200) + .then((response: Response) => { + expect(response.body).to.have.length(2); + expectSuccess(0, response); + expectBadRequest(1, response); + })); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/export_transform.ts b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/export_transform.ts new file mode 100644 index 00000000000000..dea6b1118b0f11 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/export_transform.ts @@ -0,0 +1,334 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { SavedObject } from '@kbn/core/types'; +import type { SavedObjectsExportResultDetails } from '@kbn/core/server'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +function parseNdJson(input: string): Array> { + return input.split('\n').map((str) => JSON.parse(str)); +} + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['common', 'svlCommonPage', 'savedObjects']); + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const svlCommonApi = getService('svlCommonApi'); + const testSubjects = getService('testSubjects'); + + describe('export transforms', () => { + describe('root objects export transforms', () => { + before(async () => { + await esArchiver.load( + 'test/functional/fixtures/es_archiver/saved_objects_management/export_transform' + ); + await pageObjects.svlCommonPage.login(); + await pageObjects.common.navigateToApp('management'); + await testSubjects.click('app-card-objects'); + await pageObjects.savedObjects.waitTableIsLoaded(); + }); + + after(async () => { + await esArchiver.unload( + 'test/functional/fixtures/es_archiver/saved_objects_management/export_transform' + ); + await kibanaServer.savedObjects.cleanStandardList(); + await pageObjects.svlCommonPage.forceLogout(); + }); + + it('allows to mutate the objects during an export', async () => { + await supertest + .post('/api/saved_objects/_export') + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + type: ['test-export-transform'], + excludeExportDetails: true, + }) + .expect(200) + .then((resp) => { + const objects = parseNdJson(resp.text); + expect(objects.map((obj) => ({ id: obj.id, enabled: obj.attributes.enabled }))).to.eql([ + { + id: 'type_1-obj_1', + enabled: false, + }, + { + id: 'type_1-obj_2', + enabled: false, + }, + ]); + }); + }); + + it('allows to add additional objects to an export', async () => { + await supertest + .post('/api/saved_objects/_export') + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + objects: [ + { + type: 'test-export-add', + id: 'type_2-obj_1', + }, + ], + excludeExportDetails: true, + }) + .expect(200) + .then((resp) => { + const objects = parseNdJson(resp.text); + expect(objects.map((obj) => obj.id)).to.eql(['type_2-obj_1', 'type_dep-obj_1']); + }); + }); + + it('allows to add additional objects to an export when exporting by type', async () => { + await supertest + .post('/api/saved_objects/_export') + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + type: ['test-export-add'], + excludeExportDetails: true, + }) + .expect(200) + .then((resp) => { + const objects = parseNdJson(resp.text); + expect(objects.map((obj) => obj.id)).to.eql([ + 'type_2-obj_1', + 'type_2-obj_2', + 'type_dep-obj_1', + 'type_dep-obj_2', + ]); + }); + }); + + it('returns a 400 when the type causes a transform error', async () => { + await supertest + .post('/api/saved_objects/_export') + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + type: ['test-export-transform-error'], + excludeExportDetails: true, + }) + .expect(400) + .then((resp) => { + const { attributes, ...error } = resp.body; + expect(error).to.eql({ + error: 'Bad Request', + message: 'Error transforming objects to export', + statusCode: 400, + }); + expect(attributes.cause).to.eql('Error during transform'); + expect(attributes.objects.map((obj: any) => obj.id)).to.eql(['type_4-obj_1']); + }); + }); + + it('returns a 400 when the type causes an invalid transform', async () => { + await supertest + .post('/api/saved_objects/_export') + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + type: ['test-export-invalid-transform'], + excludeExportDetails: true, + }) + .expect(400) + .then((resp) => { + expect(resp.body).to.eql({ + error: 'Bad Request', + message: 'Invalid transform performed on objects to export', + statusCode: 400, + attributes: { + objectKeys: ['test-export-invalid-transform|type_3-obj_1'], + }, + }); + }); + }); + }); + + describe('nested export transforms', () => { + before(async () => { + await esArchiver.load( + 'test/functional/fixtures/es_archiver/saved_objects_management/nested_export_transform' + ); + await pageObjects.svlCommonPage.login(); + await pageObjects.common.navigateToApp('management'); + await testSubjects.click('app-card-objects'); + await pageObjects.savedObjects.waitTableIsLoaded(); + }); + + after(async () => { + await esArchiver.unload( + 'test/functional/fixtures/es_archiver/saved_objects_management/nested_export_transform' + ); + await kibanaServer.savedObjects.cleanStandardList(); + await pageObjects.svlCommonPage.forceLogout(); + }); + + it('execute export transforms for reference objects', async () => { + await supertest + .post('/api/saved_objects/_export') + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + objects: [ + { + type: 'test-export-transform', + id: 'type_1-obj_1', + }, + ], + includeReferencesDeep: true, + excludeExportDetails: true, + }) + .expect(200) + .then((resp) => { + const objects = parseNdJson(resp.text).sort((obj1, obj2) => + obj1.id.localeCompare(obj2.id) + ); + expect(objects.map((obj) => obj.id)).to.eql([ + 'type_1-obj_1', + 'type_1-obj_2', + 'type_2-obj_1', + 'type_dep-obj_1', + ]); + + expect(objects[0].attributes.enabled).to.eql(false); + expect(objects[1].attributes.enabled).to.eql(false); + }); + }); + }); + + describe('isExportable API', () => { + before(async () => { + await esArchiver.load( + 'test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion' + ); + await pageObjects.svlCommonPage.login(); + await pageObjects.common.navigateToApp('management'); + await testSubjects.click('app-card-objects'); + await pageObjects.savedObjects.waitTableIsLoaded(); + }); + + after(async () => { + await esArchiver.unload( + 'test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion' + ); + await kibanaServer.savedObjects.cleanStandardList(); + await pageObjects.svlCommonPage.forceLogout(); + }); + + it('should only export objects returning `true` for `isExportable`', async () => { + await supertest + .post('/api/saved_objects/_export') + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + objects: [ + { + type: 'test-is-exportable', + id: '1', + }, + ], + includeReferencesDeep: true, + excludeExportDetails: true, + }) + .expect(200) + .then((resp) => { + const objects = parseNdJson(resp.text).sort((obj1, obj2) => + obj1.id.localeCompare(obj2.id) + ); + expect(objects.map((obj) => `${obj.type}:${obj.id}`)).to.eql([ + 'test-is-exportable:1', + 'test-is-exportable:3', + 'test-is-exportable:5', + ]); + }); + }); + + it('lists objects that got filtered', async () => { + await supertest + .post('/api/saved_objects/_export') + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + objects: [ + { + type: 'test-is-exportable', + id: '1', + }, + ], + includeReferencesDeep: true, + excludeExportDetails: false, + }) + .expect(200) + .then((resp) => { + const objects = parseNdJson(resp.text); + const exportDetails = objects[ + objects.length - 1 + ] as unknown as SavedObjectsExportResultDetails; + + expect(exportDetails.excludedObjectsCount).to.eql(2); + expect(exportDetails.excludedObjects).to.eql([ + { + type: 'test-is-exportable', + id: '2', + reason: 'excluded', + }, + { + type: 'test-is-exportable', + id: '4', + reason: 'excluded', + }, + ]); + }); + }); + + it('excludes objects if `isExportable` throws', async () => { + await supertest + .post('/api/saved_objects/_export') + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + objects: [ + { + type: 'test-is-exportable', + id: '5', + }, + { + type: 'test-is-exportable', + id: 'error', + }, + ], + includeReferencesDeep: true, + excludeExportDetails: false, + }) + .expect(200) + .then((resp) => { + const objects = parseNdJson(resp.text); + expect(objects.length).to.eql(2); + expect([objects[0]].map((obj) => `${obj.type}:${obj.id}`)).to.eql([ + 'test-is-exportable:5', + ]); + const exportDetails = objects[ + objects.length - 1 + ] as unknown as SavedObjectsExportResultDetails; + expect(exportDetails.excludedObjects).to.eql([ + { + type: 'test-is-exportable', + id: 'error', + reason: 'predicate_error', + }, + ]); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/exports/_import_both_types.ndjson b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/exports/_import_both_types.ndjson new file mode 100644 index 00000000000000..d72511238e38f3 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/exports/_import_both_types.ndjson @@ -0,0 +1,2 @@ +{"attributes":{"title": "Test Import warnings 1"},"id":"08ff1d6a-a2e7-11e7-bb30-2e3be9be6a73","migrationVersion":{"visualization":"7.0.0"},"references":[],"type":"test_import_warning_1","version":1} +{"attributes":{"title": "Test Import warnings 2"},"id":"77bb1e6a-a2e7-11e7-bb30-2e3be9be6a73","migrationVersion":{"visualization":"7.0.0"},"references":[],"type":"test_import_warning_2","version":1} diff --git a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/exports/_import_hidden_from_http_apis.ndjson b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/exports/_import_hidden_from_http_apis.ndjson new file mode 100644 index 00000000000000..1c509c9f75aa37 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/exports/_import_hidden_from_http_apis.ndjson @@ -0,0 +1,2 @@ +{"attributes": {"title": "I am hidden from http apis but the client can still see me"},"id": "hidden-from-http-apis-import1","references": [],"type":"test-hidden-from-http-apis-importable-exportable","version": 1} +{"attributes": {"title": "I am not hidden from http apis"},"id": "not-hidden-from-http-apis-import1","references": [],"type": "test-not-hidden-from-http-apis-importable-exportable","version": 1} diff --git a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/exports/_import_hidden_importable.ndjson b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/exports/_import_hidden_importable.ndjson new file mode 100644 index 00000000000000..a74585c07b8687 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/exports/_import_hidden_importable.ndjson @@ -0,0 +1 @@ +{"attributes": { "title": "Hidden Saved object type that is importable/exportable." }, "id":"ff3733a0-9fty-11e7-ahb3-3dcb94193fab", "references":[], "type":"test-hidden-importable-exportable", "version":1} diff --git a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/exports/_import_hidden_non_importable.ndjson b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/exports/_import_hidden_non_importable.ndjson new file mode 100644 index 00000000000000..25eea91b8bc435 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/exports/_import_hidden_non_importable.ndjson @@ -0,0 +1 @@ +{"attributes": { "title": "Hidden Saved object type that is not importable/exportable." },"id":"op3767a1-9rcg-53u7-jkb3-3dnb74193awc","references":[],"type":"test-hidden-non-importable-exportable","version":1} diff --git a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/exports/_import_non_visible_in_management.ndjson b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/exports/_import_non_visible_in_management.ndjson new file mode 100644 index 00000000000000..754848a99d03d8 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/exports/_import_non_visible_in_management.ndjson @@ -0,0 +1 @@ +{"attributes": { "title": "Saved object type that is not visible in management" }, "id":"ff3773b0-9ate-11e7-ahb3-3dcb94193fab", "references":[], "type":"test-not-visible-in-management", "version":1} diff --git a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/exports/_import_type_1.ndjson b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/exports/_import_type_1.ndjson new file mode 100644 index 00000000000000..f24f73880190a3 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/exports/_import_type_1.ndjson @@ -0,0 +1 @@ +{"attributes":{"title": "Test Import warnings 1"},"id":"08ff1d6a-a2e7-11e7-bb30-2e3be9be6a73","migrationVersion":{"visualization":"7.0.0"},"references":[],"type":"test_import_warning_1","version":1} diff --git a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/exports/_import_type_2.ndjson b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/exports/_import_type_2.ndjson new file mode 100644 index 00000000000000..15efd8a6ce03d9 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/exports/_import_type_2.ndjson @@ -0,0 +1 @@ +{"attributes":{"title": "Test Import warnings 2"},"id":"77bb1e6a-a2e7-11e7-bb30-2e3be9be6a73","migrationVersion":{"visualization":"7.0.0"},"references":[],"type":"test_import_warning_2","version":1} diff --git a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/find.ts b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/find.ts new file mode 100644 index 00000000000000..e2787135b093ad --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/find.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['common', 'svlCommonPage', 'savedObjects']); + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const svlCommonApi = getService('svlCommonApi'); + const testSubjects = getService('testSubjects'); + + describe('find', () => { + describe('saved objects with hidden type', () => { + before(async () => { + await esArchiver.load( + 'test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects' + ); + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/saved_objects_management/hidden_saved_objects' + ); + await pageObjects.svlCommonPage.login(); + await pageObjects.common.navigateToApp('management'); + await testSubjects.click('app-card-objects'); + await pageObjects.savedObjects.waitTableIsLoaded(); + }); + + after(async () => { + await esArchiver.unload( + 'test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects' + ); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/saved_objects_management/hidden_saved_objects' + ); + // emptyKibanaIndex fails in Serverless with + // "index_not_found_exception: no such index [.kibana_ingest]", + // so it was switched to `savedObjects.cleanStandardList() + await kibanaServer.savedObjects.cleanStandardList(); + await pageObjects.svlCommonPage.forceLogout(); + }); + + it('returns saved objects with importableAndExportable types', async () => + await supertest + .get('/api/kibana/management/saved_objects/_find?type=test-hidden-importable-exportable') + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .expect(200) + .then((resp) => { + expect( + resp.body.saved_objects.map((so: { id: string; type: string }) => ({ + id: so.id, + type: so.type, + })) + ).to.eql([ + { + type: 'test-hidden-importable-exportable', + id: 'ff3733a0-9fty-11e7-ahb3-3dcb94193fab', + }, + ]); + })); + + it('returns empty response for non importableAndExportable types', async () => + await supertest + .get( + '/api/kibana/management/saved_objects/_find?type=test-hidden-non-importable-exportable' + ) + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .expect(200) + .then((resp) => { + expect(resp.body.saved_objects).to.eql([]); + })); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/hidden_from_http_apis.ts b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/hidden_from_http_apis.ts new file mode 100644 index 00000000000000..404773e584a2a9 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/hidden_from_http_apis.ts @@ -0,0 +1,219 @@ +/* + * 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 { join } from 'path'; +import expect from '@kbn/expect'; +import type { Response } from 'supertest'; +import { SavedObject } from '@kbn/core/types'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +function parseNdJson(input: string): Array> { + return input.split('\n').map((str) => JSON.parse(str)); +} + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['common', 'svlCommonPage', 'savedObjects']); + const supertest = getService('supertest'); + const kbnServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + const svlCommonApi = getService('svlCommonApi'); + const testSubjects = getService('testSubjects'); + + describe('types with `hiddenFromHttpApis` ', () => { + before(async () => { + await kbnServer.savedObjects.cleanStandardList(); + await kbnServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/saved_objects_management/hidden_from_http_apis' + ); + await pageObjects.svlCommonPage.login(); + await pageObjects.common.navigateToApp('management'); + await testSubjects.click('app-card-objects'); + await pageObjects.savedObjects.waitTableIsLoaded(); + }); + + after(async () => { + // We cannot use `kbnServer.importExport.unload` to clean up test fixtures. + // `kbnServer.importExport.unload` uses the global SOM `delete` HTTP API + // and will throw on `hiddenFromHttpApis:true` objects + await esArchiver.unload( + 'test/functional/fixtures/es_archiver/saved_objects_management/hidden_from_http_apis' + ); + await pageObjects.svlCommonPage.forceLogout(); + }); + + describe('APIS', () => { + const hiddenFromHttpApisType = { + type: 'test-hidden-from-http-apis-importable-exportable', + id: 'hidden-from-http-apis-1', + }; + const notHiddenFromHttpApisType = { + type: 'test-not-hidden-from-http-apis-importable-exportable', + id: 'not-hidden-from-http-apis-1', + }; + + describe('_bulk_get', () => { + describe('saved objects with hiddenFromHttpApis type', () => { + const URL = '/api/kibana/management/saved_objects/_bulk_get'; + + it('should return 200 for types that are not hidden from the http apis', async () => + await supertest + .post(URL) + .send([notHiddenFromHttpApisType]) + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .expect(200) + .then((response: Response) => { + expect(response.body).to.have.length(1); + const { type, id, meta, error } = response.body[0]; + expect(type).to.eql(notHiddenFromHttpApisType.type); + expect(id).to.eql(notHiddenFromHttpApisType.id); + expect(meta).to.not.equal(undefined); + expect(error).to.equal(undefined); + })); + + it('should return 200 for types that are hidden from the http apis', async () => + await supertest + .post(URL) + .send([hiddenFromHttpApisType]) + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .expect(200) + .then((response: Response) => { + expect(response.body).to.have.length(1); + const { type, id, meta, error } = response.body[0]; + expect(type).to.eql(hiddenFromHttpApisType.type); + expect(id).to.eql(hiddenFromHttpApisType.id); + expect(meta).to.not.equal(undefined); + expect(error).to.equal(undefined); + })); + + it('should return 200 for a mix of types', async () => + await supertest + .post(URL) + .send([hiddenFromHttpApisType, notHiddenFromHttpApisType]) + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .expect(200) + .expect(200) + .then((response: Response) => { + expect(response.body).to.have.length(2); + const { type, id, meta, error } = response.body[0]; + expect(type).to.eql(hiddenFromHttpApisType.type); + expect(id).to.eql(hiddenFromHttpApisType.id); + expect(meta).to.not.equal(undefined); + expect(error).to.equal(undefined); + })); + }); + }); + + describe('find', () => { + it('returns saved objects registered as hidden from the http Apis', async () => { + await supertest + .get(`/api/kibana/management/saved_objects/_find?type=${hiddenFromHttpApisType.type}`) + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .expect(200) + .then((resp) => { + expect( + resp.body.saved_objects.map((so: { id: string; type: string }) => ({ + id: so.id, + type: so.type, + })) + ).to.eql([ + { + id: 'hidden-from-http-apis-1', + type: 'test-hidden-from-http-apis-importable-exportable', + }, + { + id: 'hidden-from-http-apis-2', + type: 'test-hidden-from-http-apis-importable-exportable', + }, + ]); + }); + }); + }); + + describe('export', () => { + it('allows to export them directly by id', async () => { + await supertest + .post('/api/saved_objects/_export') + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + objects: [ + { + type: 'test-hidden-from-http-apis-importable-exportable', + id: 'hidden-from-http-apis-1', + }, + ], + excludeExportDetails: true, + }) + .expect(200) + .then((resp) => { + const objects = parseNdJson(resp.text); + expect(objects.map((obj) => obj.id)).to.eql(['hidden-from-http-apis-1']); + }); + }); + + it('allows to export them directly by type', async () => { + await supertest + .post('/api/saved_objects/_export') + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + type: ['test-hidden-from-http-apis-importable-exportable'], + excludeExportDetails: true, + }) + .expect(200) + .then((resp) => { + const objects = parseNdJson(resp.text); + expect(objects.map((obj) => obj.id)).to.eql([ + 'hidden-from-http-apis-1', + 'hidden-from-http-apis-2', + ]); + }); + }); + }); + + describe('import', () => { + it('allows to import them', async () => { + await supertest + .post('/api/saved_objects/_import') + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .attach('file', join(__dirname, './exports/_import_hidden_from_http_apis.ndjson')) + .expect(200) + .then((resp) => { + expect(resp.body).to.eql({ + success: true, + successCount: 2, + successResults: [ + { + id: 'hidden-from-http-apis-import1', + meta: { + title: 'I am hidden from http apis but the client can still see me', + }, + type: 'test-hidden-from-http-apis-importable-exportable', + managed: false, + }, + { + id: 'not-hidden-from-http-apis-import1', + meta: { + title: 'I am not hidden from http apis', + }, + type: 'test-not-hidden-from-http-apis-importable-exportable', + managed: false, + }, + ], + warnings: [], + }); + }); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/hidden_types.ts b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/hidden_types.ts new file mode 100644 index 00000000000000..767e833ecc20f7 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/hidden_types.ts @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import path from 'path'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +const fixturePaths = { + hiddenImportable: path.join(__dirname, 'exports', '_import_hidden_importable.ndjson'), + hiddenNonImportable: path.join(__dirname, 'exports', '_import_hidden_non_importable.ndjson'), +}; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'svlCommonPage', 'savedObjects']); + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const svlCommonApi = getService('svlCommonApi'); + const testSubjects = getService('testSubjects'); + + describe('saved objects management with hidden types', () => { + before(async () => { + await esArchiver.load( + 'test/functional/fixtures/es_archiver/saved_objects_management/hidden_types' + ); + await PageObjects.svlCommonPage.login(); + await PageObjects.common.navigateToApp('management'); + await testSubjects.click('app-card-objects'); + await PageObjects.savedObjects.waitTableIsLoaded(); + }); + + after(async () => { + await esArchiver.unload( + 'test/functional/fixtures/es_archiver/saved_objects_management/hidden_types' + ); + await kibanaServer.savedObjects.cleanStandardList(); + await PageObjects.svlCommonPage.forceLogout(); + }); + + beforeEach(async () => { + // await PageObjects.svlCommonPage.login(); + await PageObjects.common.navigateToApp('management'); + await testSubjects.click('app-card-objects'); + await PageObjects.savedObjects.waitTableIsLoaded(); + }); + + describe('API calls', () => { + it('should flag the object as hidden in its meta', async () => { + await supertest + .get('/api/kibana/management/saved_objects/_find?type=test-actions-export-hidden') + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .expect(200) + .then((resp) => { + expect( + resp.body.saved_objects.map((obj: any) => ({ + id: obj.id, + type: obj.type, + hidden: obj.meta.hiddenType, + })) + ).to.eql([ + { + id: 'obj_1', + type: 'test-actions-export-hidden', + hidden: true, + }, + { + id: 'obj_2', + type: 'test-actions-export-hidden', + hidden: true, + }, + ]); + }); + }); + }); + + describe('Delete modal', () => { + it('should display a warning then trying to delete hidden saved objects', async () => { + await PageObjects.savedObjects.clickCheckboxByTitle('A Pie'); + await PageObjects.savedObjects.clickCheckboxByTitle('A Dashboard'); + await PageObjects.savedObjects.clickCheckboxByTitle('hidden object 1'); + + await PageObjects.savedObjects.clickDelete({ confirmDelete: false }); + expect(await testSubjects.exists('cannotDeleteObjectsConfirmWarning')).to.eql(true); + }); + + it('should not delete the hidden objects when performing the operation', async () => { + await PageObjects.savedObjects.clickCheckboxByTitle('A Pie'); + await PageObjects.savedObjects.clickCheckboxByTitle('hidden object 1'); + + await PageObjects.savedObjects.clickDelete({ confirmDelete: true }); + + const objectNames = (await PageObjects.savedObjects.getTableSummary()).map( + (obj) => obj.title + ); + expect(objectNames.includes('hidden object 1')).to.eql(true); + expect(objectNames.includes('A Pie')).to.eql(false); + }); + }); + + describe('importing hidden types', () => { + describe('importable/exportable hidden type', () => { + it('imports objects successfully', async () => { + await PageObjects.savedObjects.importFile(fixturePaths.hiddenImportable); + await PageObjects.savedObjects.checkImportSucceeded(); + }); + + it('shows test-hidden-importable-exportable in table', async () => { + await PageObjects.savedObjects.searchForObject( + 'type:(test-hidden-importable-exportable)' + ); + const results = await PageObjects.savedObjects.getTableSummary(); + expect(results.length).to.be(1); + + const { title } = results[0]; + expect(title).to.be( + 'test-hidden-importable-exportable [id=ff3733a0-9fty-11e7-ahb3-3dcb94193fab]' + ); + }); + }); + + describe('non-importable/exportable hidden type', () => { + it('fails to import object', async () => { + await PageObjects.savedObjects.importFile(fixturePaths.hiddenNonImportable); + await PageObjects.savedObjects.checkImportSucceeded(); + + const errorsCount = await PageObjects.savedObjects.getImportErrorsCount(); + expect(errorsCount).to.be(1); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/import_warnings.ts b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/import_warnings.ts new file mode 100644 index 00000000000000..ca3d81f2c45511 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/import_warnings.ts @@ -0,0 +1,85 @@ +/* + * 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 path from 'path'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'svlCommonPage', 'savedObjects']); + + const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); + + describe('import warnings', () => { + before(async () => { + // emptyKibanaIndex fails in Serverless with + // "index_not_found_exception: no such index [.kibana_ingest]", + // so it was switched to `savedObjects.cleanStandardList() + await kibanaServer.savedObjects.cleanStandardList(); + }); + + beforeEach(async () => { + await PageObjects.svlCommonPage.login(); + await PageObjects.common.navigateToApp('management'); + await testSubjects.click('app-card-objects'); + await PageObjects.savedObjects.waitTableIsLoaded(); + }); + + it('should display simple warnings', async () => { + await PageObjects.savedObjects.importFile( + path.join(__dirname, 'exports', '_import_type_1.ndjson') + ); + + await PageObjects.savedObjects.checkImportSucceeded(); + const warnings = await PageObjects.savedObjects.getImportWarnings(); + + expect(warnings).to.eql([ + { + message: 'warning for test_import_warning_1', + type: 'simple', + }, + ]); + }); + + it('should display action warnings', async () => { + await PageObjects.savedObjects.importFile( + path.join(__dirname, 'exports', '_import_type_2.ndjson') + ); + + await PageObjects.savedObjects.checkImportSucceeded(); + const warnings = await PageObjects.savedObjects.getImportWarnings(); + + expect(warnings).to.eql([ + { + type: 'action_required', + message: 'warning for test_import_warning_2', + }, + ]); + }); + + it('should display warnings coming from multiple types', async () => { + await PageObjects.savedObjects.importFile( + path.join(__dirname, 'exports', '_import_both_types.ndjson') + ); + + await PageObjects.savedObjects.checkImportSucceeded(); + const warnings = await PageObjects.savedObjects.getImportWarnings(); + + expect(warnings).to.eql([ + { + message: 'warning for test_import_warning_1', + type: 'simple', + }, + { + type: 'action_required', + message: 'warning for test_import_warning_2', + }, + ]); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/index.ts b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/index.ts new file mode 100644 index 00000000000000..7549224938426b --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/index.ts @@ -0,0 +1,22 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Saved Objects Management', function () { + this.tags('skipMKI'); + loadTestFile(require.resolve('./find')); + loadTestFile(require.resolve('./scroll_count')); + loadTestFile(require.resolve('./bulk_get')); + loadTestFile(require.resolve('./export_transform')); + loadTestFile(require.resolve('./import_warnings')); + loadTestFile(require.resolve('./hidden_types')); + loadTestFile(require.resolve('./visible_in_management')); + loadTestFile(require.resolve('./hidden_from_http_apis')); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/scroll_count.ts b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/scroll_count.ts new file mode 100644 index 00000000000000..d6d173b646563b --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/scroll_count.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['common', 'svlCommonPage', 'savedObjects']); + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const apiUrl = '/api/kibana/management/saved_objects/scroll/counts'; + const svlCommonApi = getService('svlCommonApi'); + const testSubjects = getService('testSubjects'); + + describe('scroll_count', () => { + describe('saved objects with hidden type', () => { + before(async () => { + await esArchiver.load( + 'test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects' + ); + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/saved_objects_management/hidden_saved_objects' + ); + await pageObjects.svlCommonPage.login(); + await pageObjects.common.navigateToApp('management'); + await testSubjects.click('app-card-objects'); + await pageObjects.savedObjects.waitTableIsLoaded(); + }); + after(async () => { + await esArchiver.unload( + 'test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects' + ); + await kibanaServer.savedObjects.clean({ + types: ['test-hidden-importable-exportable'], + }); + }); + + it('only counts hidden types that are importableAndExportable', async () => { + const res = await supertest + .post(apiUrl) + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + typesToInclude: [ + 'test-hidden-non-importable-exportable', + 'test-hidden-importable-exportable', + ], + }) + .expect(200); + + expect(res.body).to.eql({ + 'test-hidden-importable-exportable': 1, + 'test-hidden-non-importable-exportable': 0, + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/visible_in_management.ts b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/visible_in_management.ts new file mode 100644 index 00000000000000..f0e4486a5254bb --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/visible_in_management.ts @@ -0,0 +1,148 @@ +/* + * 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 { join } from 'path'; +import expect from '@kbn/expect'; +import type { Response } from 'supertest'; +import { SavedObject } from '@kbn/core/server'; +import type { SavedObjectManagementTypeInfo } from '@kbn/saved-objects-management-plugin/common/types'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +function parseNdJson(input: string): Array> { + return input.split('\n').map((str) => JSON.parse(str)); +} + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['common', 'svlCommonPage', 'savedObjects']); + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const svlCommonApi = getService('svlCommonApi'); + const testSubjects = getService('testSubjects'); + + describe('types with `visibleInManagement` ', () => { + before(async () => { + await esArchiver.load( + 'test/functional/fixtures/es_archiver/saved_objects_management/visible_in_management' + ); + await pageObjects.svlCommonPage.login(); + await pageObjects.common.navigateToApp('management'); + await testSubjects.click('app-card-objects'); + await pageObjects.savedObjects.waitTableIsLoaded(); + }); + + after(async () => { + await esArchiver.unload( + 'test/functional/fixtures/es_archiver/saved_objects_management/visible_in_management' + ); + await pageObjects.svlCommonPage.forceLogout(); + }); + + describe('export', () => { + it('allows to export them directly by id', async () => { + await supertest + .post('/api/saved_objects/_export') + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + objects: [ + { + type: 'test-not-visible-in-management', + id: 'vim-1', + }, + ], + excludeExportDetails: true, + }) + .expect(200) + .then((resp) => { + const objects = parseNdJson(resp.text); + expect(objects.map((obj) => obj.id)).to.eql(['vim-1']); + }); + }); + + it('allows to export them directly by type', async () => { + await supertest + .post('/api/saved_objects/_export') + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + type: ['test-not-visible-in-management'], + excludeExportDetails: true, + }) + .expect(200) + .then((resp) => { + const objects = parseNdJson(resp.text); + expect(objects.map((obj) => obj.id)).to.eql(['vim-1']); + }); + }); + }); + + describe('import', () => { + it('allows to import them', async () => { + await supertest + .post('/api/saved_objects/_import') + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .attach('file', join(__dirname, './exports/_import_non_visible_in_management.ndjson')) + .expect(200) + .then((resp) => { + expect(resp.body).to.eql({ + success: true, + successCount: 1, + successResults: [ + { + id: 'ff3773b0-9ate-11e7-ahb3-3dcb94193fab', + meta: { + title: 'Saved object type that is not visible in management', + }, + type: 'test-not-visible-in-management', + managed: false, + }, + ], + warnings: [], + }); + }); + }); + }); + + describe('savedObjects management APIS', () => { + describe('GET /api/kibana/management/saved_objects/_allowed_types', () => { + let types: SavedObjectManagementTypeInfo[]; + + before(async () => { + await supertest + .get('/api/kibana/management/saved_objects/_allowed_types') + .set(svlCommonApi.getCommonRequestHeader()) + .set(svlCommonApi.getInternalRequestHeader()) + .expect(200) + .then((response: Response) => { + types = response.body.types as SavedObjectManagementTypeInfo[]; + }); + }); + + it('should only return types that are `visibleInManagement: true`', () => { + const typeNames = types.map((type) => type.name); + + expect(typeNames.includes('test-is-exportable')).to.eql(true); + expect(typeNames.includes('test-visible-in-management')).to.eql(true); + expect(typeNames.includes('test-not-visible-in-management')).to.eql(false); + }); + + it('should return displayName for types specifying it', () => { + const typeWithDisplayName = types.find((type) => type.name === 'test-with-display-name'); + expect(typeWithDisplayName !== undefined).to.eql(true); + expect(typeWithDisplayName!.displayName).to.eql('my display name'); + + const typeWithoutDisplayName = types.find( + (type) => type.name === 'test-visible-in-management' + ); + expect(typeWithoutDisplayName !== undefined).to.eql(true); + expect(typeWithoutDisplayName!.displayName).to.eql('test-visible-in-management'); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/observability/config.saved_objects_management.ts b/x-pack/test_serverless/functional/test_suites/observability/config.saved_objects_management.ts new file mode 100644 index 00000000000000..1e5a1ef7155c22 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/observability/config.saved_objects_management.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { REPO_ROOT } from '@kbn/repo-info'; +import { findTestPluginPaths } from '@kbn/test'; +import { resolve } from 'path'; +import { createTestConfig } from '../../config.base'; + +export default createTestConfig({ + serverlessProject: 'oblt', + testFiles: [require.resolve('../common/saved_objects_management')], + junit: { + reportName: 'Serverless Search Saved Objects Management Functional Tests', + }, + kbnServerArgs: findTestPluginPaths([resolve(REPO_ROOT, 'test/plugin_functional/plugins')]), + + // include settings from project controller + // https://github.com/elastic/project-controller/blob/main/internal/project/esproject/config/elasticsearch.yml + esServerArgs: [], +}); diff --git a/x-pack/test_serverless/functional/test_suites/search/config.saved_objects_management.ts b/x-pack/test_serverless/functional/test_suites/search/config.saved_objects_management.ts new file mode 100644 index 00000000000000..e3f8b4694e3ca5 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/search/config.saved_objects_management.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { REPO_ROOT } from '@kbn/repo-info'; +import { findTestPluginPaths } from '@kbn/test'; +import { resolve } from 'path'; +import { createTestConfig } from '../../config.base'; + +export default createTestConfig({ + serverlessProject: 'es', + testFiles: [require.resolve('../common/saved_objects_management')], + junit: { + reportName: 'Serverless Search Saved Objects Management Functional Tests', + }, + kbnServerArgs: findTestPluginPaths([resolve(REPO_ROOT, 'test/plugin_functional/plugins')]), + + // include settings from project controller + // https://github.com/elastic/project-controller/blob/main/internal/project/esproject/config/elasticsearch.yml + esServerArgs: [], +}); diff --git a/x-pack/test_serverless/functional/test_suites/security/config.saved_objects_management.ts b/x-pack/test_serverless/functional/test_suites/security/config.saved_objects_management.ts new file mode 100644 index 00000000000000..68c3ca9a73be4c --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/security/config.saved_objects_management.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { REPO_ROOT } from '@kbn/repo-info'; +import { findTestPluginPaths } from '@kbn/test'; +import { resolve } from 'path'; +import { createTestConfig } from '../../config.base'; + +export default createTestConfig({ + serverlessProject: 'security', + testFiles: [require.resolve('../common/saved_objects_management')], + junit: { + reportName: 'Serverless Search Saved Objects Management Functional Tests', + }, + kbnServerArgs: findTestPluginPaths([resolve(REPO_ROOT, 'test/plugin_functional/plugins')]), + + // include settings from project controller + // https://github.com/elastic/project-controller/blob/main/internal/project/esproject/config/elasticsearch.yml + esServerArgs: [], +}); diff --git a/x-pack/test_serverless/tsconfig.json b/x-pack/test_serverless/tsconfig.json index 43db4b65f72e86..8e2a2bf7d388ef 100644 --- a/x-pack/test_serverless/tsconfig.json +++ b/x-pack/test_serverless/tsconfig.json @@ -88,5 +88,6 @@ "@kbn/core", "@kbn/alerting-plugin", "@kbn/ftr-common-functional-ui-services", + "@kbn/saved-objects-management-plugin", ] } diff --git a/yarn.lock b/yarn.lock index 6249cc68cf95f0..be03649c088e02 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7602,15 +7602,15 @@ resolved "https://registry.yarnpkg.com/@redux-saga/types/-/types-1.1.0.tgz#0e81ce56b4883b4b2a3001ebe1ab298b84237204" integrity sha512-afmTuJrylUU/0OtqzaRkbyYFFNgCF73Bvel/sw90pvGrWIZ+vyoIJqA6eMSoA6+nb443kTmulmBtC9NerXboNg== -"@reduxjs/toolkit@1.7.2": - version "1.7.2" - resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.7.2.tgz#b428aaef92582379464f9de698dbb71957eafb02" - integrity sha512-wwr3//Ar8ZhM9bS58O+HCIaMlR4Y6SNHfuszz9hKnQuFIKvwaL3Kmjo6fpDKUOjo4Lv54Yi299ed8rofCJ/Vjw== +"@reduxjs/toolkit@1.9.7": + version "1.9.7" + resolved "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.7.tgz" + integrity sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ== dependencies: - immer "^9.0.7" - redux "^4.1.2" - redux-thunk "^2.4.1" - reselect "^4.1.5" + immer "^9.0.21" + redux "^4.2.1" + redux-thunk "^2.4.2" + reselect "^4.1.8" "@remix-run/router@1.6.3": version "1.6.3" @@ -15630,10 +15630,10 @@ elastic-apm-node@3.46.0: traverse "^0.6.6" unicode-byte-truncate "^1.0.0" -elastic-apm-node@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-4.3.0.tgz#449cd3d3154858fbe0bbf223d3a1442aa5a354bd" - integrity sha512-LLB+RH12cFIdKZxxW6oOt3aL+8oEYNaHILol31dFb2SFdCKCWceMfUSzESTuPiImdyRTi5A+2Iw+46Fgt4caPQ== +elastic-apm-node@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-4.4.0.tgz#41452d8c0b040e6df99da71704c3d62783bb0f78" + integrity sha512-/CFP1DjK0eM8i4pSt8k10fX8I2UWh9u0Ycr1SXdy3t7ZQfI1FbM18IKFeMjktJIHbk2W+6XOxAY1RN8QhDj+bA== dependencies: "@elastic/ecs-pino-format" "^1.5.0" "@opentelemetry/api" "^1.4.1" @@ -15653,7 +15653,7 @@ elastic-apm-node@^4.3.0: fast-safe-stringify "^2.0.7" fast-stream-to-buffer "^1.0.0" http-headers "^3.0.2" - import-in-the-middle "1.5.0" + import-in-the-middle "1.7.2" json-bigint "^1.0.0" lru-cache "^10.0.1" measured-reporting "^1.51.1" @@ -18963,10 +18963,10 @@ immediate@~3.0.5: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= -immer@^9.0.15, immer@^9.0.7: - version "9.0.15" - resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.15.tgz#0b9169e5b1d22137aba7d43f8a81a495dd1b62dc" - integrity sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ== +immer@^9.0.21: + version "9.0.21" + resolved "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz" + integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== immutable@^4.0.0: version "4.1.0" @@ -18981,10 +18981,10 @@ import-fresh@^3.1.0, import-fresh@^3.2.1, import-fresh@^3.3.0: parent-module "^1.0.0" resolve-from "^4.0.0" -import-in-the-middle@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.5.0.tgz#db939aa2b7220f6149124885160916be715d1cae" - integrity sha512-H2hqR0jImhqe9+1k8pYewDKWJHnDeRsWZk5aSztv6MIWD5glmbEOqy1JZrMUC6SJiO1M4A+nVvUUYtWzP5wPYg== +import-in-the-middle@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.7.2.tgz#31c44088271b50ecb9cacbdfb1e5732c802e0658" + integrity sha512-coz7AjRnPyKW36J6JX5Bjz1mcX7MX1H2XsEGseVcnXMdzsAbbAu0HBZhiAem+3SAmuZdi+p8OwoB2qUpTRgjOQ== dependencies: acorn "^8.8.2" acorn-import-assertions "^1.9.0" @@ -26477,20 +26477,20 @@ redux-saga@^1.1.3: dependencies: "@redux-saga/core" "^1.1.3" -redux-thunk@^2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.1.tgz#0dd8042cf47868f4b29699941de03c9301a75714" - integrity sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q== +redux-thunk@^2.4.2: + version "2.4.2" + resolved "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz" + integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q== redux-thunks@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/redux-thunks/-/redux-thunks-1.0.0.tgz#56e03b86d281a2664c884ab05c543d9ab1673658" integrity sha1-VuA7htKBomZMiEqwXFQ9mrFnNlg= -redux@^4.0.0, redux@^4.0.4, redux@^4.1.2, redux@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13" - integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA== +redux@^4.0.0, redux@^4.0.4, redux@^4.2.0, redux@^4.2.1: + version "4.2.1" + resolved "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz" + integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== dependencies: "@babel/runtime" "^7.9.2" @@ -26921,10 +26921,10 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= -reselect@^4.0.0, reselect@^4.1.5, reselect@^4.1.6: - version "4.1.6" - resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.6.tgz#19ca2d3d0b35373a74dc1c98692cdaffb6602656" - integrity sha512-ZovIuXqto7elwnxyXbBtCPo9YFEr3uJqj2rRbcOOog1bmu2Ag85M4hixSwFWyaBMKXNgvPaJ9OSu9SkBPIeJHQ== +reselect@^4.0.0, reselect@^4.1.8: + version "4.1.8" + resolved "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz" + integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ== resize-observer-polyfill@^1.5.1: version "1.5.1"