From f8e01bd3a14f606c2086955539e588484b0ab379 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 29 Apr 2020 11:58:46 -0500 Subject: [PATCH 01/30] [SIEM][NP] Fixes bug in ML signals promotion (#64720) * Add set-value as an explicit dependency This is a more robust solution than lodash's set(). * Replace lodash.set() with set-value's equivalent * Rebuild renovate config We added set-value to our dependencies. Co-authored-by: Elastic Machine --- renovate.json5 | 8 ++++++++ x-pack/package.json | 2 ++ .../signals/bulk_create_ml_signals.ts | 10 +++++++--- yarn.lock | 12 ++++++++++++ 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/renovate.json5 b/renovate.json5 index ffa006264873d3..c0ddcaf4f23c8f 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -846,6 +846,14 @@ '@types/semver', ], }, + { + groupSlug: 'set-value', + groupName: 'set-value related packages', + packageNames: [ + 'set-value', + '@types/set-value', + ], + }, { groupSlug: 'sinon', groupName: 'sinon related packages', diff --git a/x-pack/package.json b/x-pack/package.json index 604889c6094b9c..dcc9b8c61cb960 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -105,6 +105,7 @@ "@types/recompose": "^0.30.6", "@types/reduce-reducers": "^1.0.0", "@types/redux-actions": "^2.6.1", + "@types/set-value": "^2.0.0", "@types/sinon": "^7.0.13", "@types/styled-components": "^4.4.2", "@types/supertest": "^2.0.5", @@ -344,6 +345,7 @@ "rison-node": "0.3.1", "rxjs": "^6.5.3", "semver": "5.7.0", + "set-value": "^3.0.2", "squel": "^5.13.0", "stats-lite": "^2.2.0", "style-it": "^2.1.3", diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index d298f1cc7cbc66..a8cc6dc6804102 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { flow, set, omit } from 'lodash/fp'; +import { flow, omit } from 'lodash/fp'; +import set from 'set-value'; import { SearchResponse } from 'elasticsearch'; import { Logger } from '../../../../../../../src/core/server'; @@ -55,8 +56,11 @@ export const transformAnomalyFieldsToEcs = (anomaly: Anomaly): EcsAnomaly => { } const omitDottedFields = omit(errantFields.map(field => field.name)); - const setNestedFields = errantFields.map(field => set(field.name, field.value)); - const setTimestamp = set('@timestamp', new Date(timestamp).toISOString()); + const setNestedFields = errantFields.map(field => (_anomaly: Anomaly) => + set(_anomaly, field.name, field.value) + ); + const setTimestamp = (_anomaly: Anomaly) => + set(_anomaly, '@timestamp', new Date(timestamp).toISOString()); return flow(omitDottedFields, setNestedFields, setTimestamp)(anomaly); }; diff --git a/yarn.lock b/yarn.lock index ee00ef283f07c0..94e6a0a11aa993 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4798,6 +4798,11 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45" integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ== +"@types/set-value@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/set-value/-/set-value-2.0.0.tgz#63d386b103926dcf49b50e16e0f6dd49983046be" + integrity sha512-k8dCJEC80F/mbsIOZ5Hj3YSzTVVVBwMdtP/M9Rtc2TM4F5etVd+2UG8QUiAUfbXm4fABedL2tBZnrBheY7UwpA== + "@types/shot@*": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/shot/-/shot-4.0.0.tgz#7545500c489b65c69b5bc5446ba4fef3bd26af92" @@ -26910,6 +26915,13 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" +set-value@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-3.0.2.tgz#74e8ecd023c33d0f77199d415409a40f21e61b90" + integrity sha512-npjkVoz+ank0zjlV9F47Fdbjfj/PfXyVhZvGALWsyIYU/qrMzpi6avjKW3/7KeSU2Df3I46BrN1xOI1+6vW0hA== + dependencies: + is-plain-object "^2.0.4" + setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" From bac638a37e5dc670cd1c6c535e1f9fcd77ef8f64 Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Wed, 29 Apr 2020 20:00:14 +0300 Subject: [PATCH 02/30] Update jest config for coverage (#64648) * set files to track for coverage collection * increase timeout to 4h * trying to add detectOpenHandles to avoid worker stuck * update config * make config paths more common * update configs * update jest oss config * exclude 'tests' folder for coverage --- .ci/Jenkinsfile_coverage | 2 +- src/dev/jest/config.js | 1 + test/scripts/jenkins_xpack.sh | 2 +- x-pack/dev-tools/jest/create_jest_config.js | 14 ++++++++++++++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index f2a58e7b6a7ac5..c474998e6fd3db 100644 --- a/.ci/Jenkinsfile_coverage +++ b/.ci/Jenkinsfile_coverage @@ -3,7 +3,7 @@ library 'kibana-pipeline-library' kibanaLibrary.load() // load from the Jenkins instance -kibanaPipeline(timeoutMinutes: 180) { +kibanaPipeline(timeoutMinutes: 240) { catchErrors { withEnv([ 'CODE_COVERAGE=1', // Needed for multiple ci scripts, such as remote.ts, test/scripts/*.sh, schema.js, etc. diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 43a2cbd78c5023..c5387590fcf661 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -40,6 +40,7 @@ export default { ], collectCoverageFrom: [ 'src/plugins/**/*.{ts,tsx}', + '!src/plugins/**/{__test__,__snapshots__,__examples__,mocks,tests}/**/*', '!src/plugins/**/*.d.ts', 'packages/kbn-ui-framework/src/components/**/*.js', '!packages/kbn-ui-framework/src/components/index.js', diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh index 67d88b308ed91c..951ba8e22d8858 100755 --- a/test/scripts/jenkins_xpack.sh +++ b/test/scripts/jenkins_xpack.sh @@ -39,7 +39,7 @@ else # build runtime for canvas echo "NODE_ENV=$NODE_ENV" node ./legacy/plugins/canvas/scripts/shareable_runtime - node --max-old-space-size=6144 scripts/jest --ci --verbose --coverage + node --max-old-space-size=6144 scripts/jest --ci --verbose --detectOpenHandles --coverage # rename file in order to be unique one test -f ../target/kibana-coverage/jest/coverage-final.json \ && mv ../target/kibana-coverage/jest/coverage-final.json \ diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index af5ace8e3cd3b7..4f1251321b0054 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -34,6 +34,20 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) { '^test_utils/stub_web_worker': `${xPackKibanaDirectory}/test_utils/stub_web_worker.ts`, '^(!!)?file-loader!': fileMockPath, }, + collectCoverageFrom: [ + 'legacy/plugins/**/*.{js,jsx,ts,tsx}', + 'legacy/server/**/*.{js,jsx,ts,tsx}', + 'plugins/**/*.{js,jsx,ts,tsx}', + '!**/{__test__,__snapshots__,__examples__,integration_tests,tests}/**', + '!**/*.test.{js,ts,tsx}', + '!**/flot-charts/**', + '!**/test/**', + '!**/build/**', + '!**/scripts/**', + '!**/mocks/**', + '!**/plugins/apm/e2e/**', + ], + coveragePathIgnorePatterns: ['.*\\.d\\.ts'], coverageDirectory: '/../target/kibana-coverage/jest', coverageReporters: !!process.env.CODE_COVERAGE ? ['json'] : ['html'], setupFiles: [ From d8c15f5ad38c0ed480fc90cf7c8381aad04f0609 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 29 Apr 2020 18:25:48 +0100 Subject: [PATCH 03/30] [ML] Adding endpoint capability checks (#64662) * [ML] Adding endpoint capability checks * adding missing capability checks * fixing test * removing commented code * fixing functional test * fixing functional tests * changes based on review Co-authored-by: Elastic Machine --- .../plugins/ml/common/types/capabilities.ts | 32 +++- .../capabilities/check_capabilities.test.ts | 159 ++++++++++++++---- .../lib/capabilities/check_capabilities.ts | 2 + x-pack/plugins/ml/server/plugin.ts | 14 +- .../plugins/ml/server/routes/annotations.ts | 9 + .../ml/server/routes/anomaly_detectors.ts | 45 +++++ x-pack/plugins/ml/server/routes/calendars.ts | 15 ++ .../ml/server/routes/data_frame_analytics.ts | 33 ++++ .../ml/server/routes/data_visualizer.ts | 6 + x-pack/plugins/ml/server/routes/datafeeds.ts | 30 ++++ .../ml/server/routes/fields_service.ts | 7 +- .../ml/server/routes/file_data_visualizer.ts | 2 + x-pack/plugins/ml/server/routes/filters.ts | 18 ++ x-pack/plugins/ml/server/routes/indices.ts | 3 + .../ml/server/routes/job_audit_messages.ts | 6 + .../plugins/ml/server/routes/job_service.ts | 80 ++++++--- .../ml/server/routes/job_validation.ts | 12 ++ x-pack/plugins/ml/server/routes/modules.ts | 12 ++ .../ml/server/routes/notification_settings.ts | 3 + .../ml/server/routes/results_service.ts | 15 ++ x-pack/plugins/ml/server/routes/system.ts | 16 ++ x-pack/plugins/ml/server/types.ts | 4 - .../apis/ml/anomaly_detectors/create.ts | 9 +- .../apis/ml/jobs/jobs_summary.ts | 4 +- .../apis/ml/modules/setup_module.ts | 7 +- 25 files changed, 456 insertions(+), 87 deletions(-) diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index 2a449c95faa5b5..da5fd3ac252096 100644 --- a/x-pack/plugins/ml/common/types/capabilities.ts +++ b/x-pack/plugins/ml/common/types/capabilities.ts @@ -7,6 +7,7 @@ import { KibanaRequest } from 'kibana/server'; export const userMlCapabilities = { + canAccessML: false, // Anomaly Detection canGetJobs: false, canGetDatafeeds: false, @@ -18,6 +19,10 @@ export const userMlCapabilities = { canGetFilters: false, // Data Frame Analytics canGetDataFrameAnalytics: false, + // Annotations + canGetAnnotations: false, + canCreateAnnotation: false, + canDeleteAnnotation: false, }; export const adminMlCapabilities = { @@ -26,9 +31,11 @@ export const adminMlCapabilities = { canDeleteJob: false, canOpenJob: false, canCloseJob: false, + canUpdateJob: false, canForecastJob: false, + canCreateDatafeed: false, + canDeleteDatafeed: false, canStartStopDatafeed: false, - canUpdateJob: false, canUpdateDatafeed: false, canPreviewDatafeed: false, // Calendars @@ -38,8 +45,8 @@ export const adminMlCapabilities = { canCreateFilter: false, canDeleteFilter: false, // Data Frame Analytics - canDeleteDataFrameAnalytics: false, canCreateDataFrameAnalytics: false, + canDeleteDataFrameAnalytics: false, canStartStopDataFrameAnalytics: false, }; @@ -47,7 +54,9 @@ export type UserMlCapabilities = typeof userMlCapabilities; export type AdminMlCapabilities = typeof adminMlCapabilities; export type MlCapabilities = UserMlCapabilities & AdminMlCapabilities; -export const basicLicenseMlCapabilities = ['canFindFileStructure'] as Array; +export const basicLicenseMlCapabilities = ['canAccessML', 'canFindFileStructure'] as Array< + keyof MlCapabilities +>; export function getDefaultCapabilities(): MlCapabilities { return { @@ -56,6 +65,23 @@ export function getDefaultCapabilities(): MlCapabilities { }; } +export function getPluginPrivileges() { + const userMlCapabilitiesKeys = Object.keys(userMlCapabilities); + const adminMlCapabilitiesKeys = Object.keys(adminMlCapabilities); + const allMlCapabilities = [...adminMlCapabilitiesKeys, ...userMlCapabilitiesKeys]; + + return { + user: { + ui: userMlCapabilitiesKeys, + api: userMlCapabilitiesKeys.map(k => `ml:${k}`), + }, + admin: { + ui: allMlCapabilities, + api: allMlCapabilities.map(k => `ml:${k}`), + }, + }; +} + export interface MlCapabilitiesResponse { capabilities: MlCapabilities; upgradeInProgress: boolean; diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts index 5093801d2d1846..b6e95ae8373ee1 100644 --- a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts @@ -36,7 +36,7 @@ describe('check_capabilities', () => { ); const { capabilities } = await getCapabilities(); const count = Object.keys(capabilities).length; - expect(count).toBe(22); + expect(count).toBe(28); done(); }); }); @@ -49,28 +49,42 @@ describe('check_capabilities', () => { mlLicense, mlIsEnabled ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + const { + capabilities, + upgradeInProgress, + mlFeatureEnabledInSpace, + isPlatinumOrTrialLicense, + } = await getCapabilities(); expect(upgradeInProgress).toBe(false); expect(mlFeatureEnabledInSpace).toBe(true); + expect(isPlatinumOrTrialLicense).toBe(true); + + expect(capabilities.canAccessML).toBe(true); expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetFilters).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canGetAnnotations).toBe(true); + expect(capabilities.canCreateAnnotation).toBe(true); + expect(capabilities.canDeleteAnnotation).toBe(true); + expect(capabilities.canCreateJob).toBe(false); expect(capabilities.canDeleteJob).toBe(false); expect(capabilities.canOpenJob).toBe(false); expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canCreateDatafeed).toBe(false); + expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canCreateFilter).toBe(false); expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); expect(capabilities.canCreateDataFrameAnalytics).toBe(false); expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); @@ -84,28 +98,42 @@ describe('check_capabilities', () => { mlLicense, mlIsEnabled ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + const { + capabilities, + upgradeInProgress, + mlFeatureEnabledInSpace, + isPlatinumOrTrialLicense, + } = await getCapabilities(); expect(upgradeInProgress).toBe(false); expect(mlFeatureEnabledInSpace).toBe(true); + expect(isPlatinumOrTrialLicense).toBe(true); + + expect(capabilities.canAccessML).toBe(true); expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetFilters).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canGetAnnotations).toBe(true); + expect(capabilities.canCreateAnnotation).toBe(true); + expect(capabilities.canDeleteAnnotation).toBe(true); + expect(capabilities.canCreateJob).toBe(true); expect(capabilities.canDeleteJob).toBe(true); expect(capabilities.canOpenJob).toBe(true); expect(capabilities.canCloseJob).toBe(true); expect(capabilities.canForecastJob).toBe(true); - expect(capabilities.canGetDatafeeds).toBe(true); expect(capabilities.canStartStopDatafeed).toBe(true); expect(capabilities.canUpdateJob).toBe(true); + expect(capabilities.canCreateDatafeed).toBe(true); + expect(capabilities.canDeleteDatafeed).toBe(true); expect(capabilities.canUpdateDatafeed).toBe(true); expect(capabilities.canPreviewDatafeed).toBe(true); - expect(capabilities.canGetCalendars).toBe(true); expect(capabilities.canCreateCalendar).toBe(true); expect(capabilities.canDeleteCalendar).toBe(true); - expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canCreateFilter).toBe(true); expect(capabilities.canDeleteFilter).toBe(true); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); expect(capabilities.canDeleteDataFrameAnalytics).toBe(true); expect(capabilities.canCreateDataFrameAnalytics).toBe(true); expect(capabilities.canStartStopDataFrameAnalytics).toBe(true); @@ -119,28 +147,42 @@ describe('check_capabilities', () => { mlLicense, mlIsEnabled ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + const { + capabilities, + upgradeInProgress, + mlFeatureEnabledInSpace, + isPlatinumOrTrialLicense, + } = await getCapabilities(); expect(upgradeInProgress).toBe(true); expect(mlFeatureEnabledInSpace).toBe(true); + expect(isPlatinumOrTrialLicense).toBe(true); + + expect(capabilities.canAccessML).toBe(true); expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetFilters).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canGetAnnotations).toBe(true); + expect(capabilities.canCreateAnnotation).toBe(false); + expect(capabilities.canDeleteAnnotation).toBe(false); + expect(capabilities.canCreateJob).toBe(false); expect(capabilities.canDeleteJob).toBe(false); expect(capabilities.canOpenJob).toBe(false); expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canCreateDatafeed).toBe(false); + expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canCreateFilter).toBe(false); expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); expect(capabilities.canCreateDataFrameAnalytics).toBe(false); expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); @@ -154,28 +196,42 @@ describe('check_capabilities', () => { mlLicense, mlIsEnabled ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + const { + capabilities, + upgradeInProgress, + mlFeatureEnabledInSpace, + isPlatinumOrTrialLicense, + } = await getCapabilities(); expect(upgradeInProgress).toBe(true); expect(mlFeatureEnabledInSpace).toBe(true); + expect(isPlatinumOrTrialLicense).toBe(true); + + expect(capabilities.canAccessML).toBe(true); expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetFilters).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canGetAnnotations).toBe(true); + expect(capabilities.canCreateAnnotation).toBe(false); + expect(capabilities.canDeleteAnnotation).toBe(false); + expect(capabilities.canCreateJob).toBe(false); expect(capabilities.canDeleteJob).toBe(false); expect(capabilities.canOpenJob).toBe(false); expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canCreateDatafeed).toBe(false); + expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canCreateFilter).toBe(false); expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); expect(capabilities.canCreateDataFrameAnalytics).toBe(false); expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); @@ -189,28 +245,42 @@ describe('check_capabilities', () => { mlLicense, mlIsNotEnabled ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + const { + capabilities, + upgradeInProgress, + mlFeatureEnabledInSpace, + isPlatinumOrTrialLicense, + } = await getCapabilities(); expect(upgradeInProgress).toBe(false); expect(mlFeatureEnabledInSpace).toBe(false); + expect(isPlatinumOrTrialLicense).toBe(true); + + expect(capabilities.canAccessML).toBe(false); expect(capabilities.canGetJobs).toBe(false); + expect(capabilities.canGetDatafeeds).toBe(false); + expect(capabilities.canGetCalendars).toBe(false); + expect(capabilities.canFindFileStructure).toBe(false); + expect(capabilities.canGetFilters).toBe(false); + expect(capabilities.canGetDataFrameAnalytics).toBe(false); + expect(capabilities.canGetAnnotations).toBe(false); + expect(capabilities.canCreateAnnotation).toBe(false); + expect(capabilities.canDeleteAnnotation).toBe(false); + expect(capabilities.canCreateJob).toBe(false); expect(capabilities.canDeleteJob).toBe(false); expect(capabilities.canOpenJob).toBe(false); expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canCreateDatafeed).toBe(false); + expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canCreateFilter).toBe(false); expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(false); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); expect(capabilities.canCreateDataFrameAnalytics).toBe(false); expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); @@ -225,28 +295,43 @@ describe('check_capabilities', () => { mlLicenseBasic, mlIsNotEnabled ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + const { + capabilities, + upgradeInProgress, + mlFeatureEnabledInSpace, + isPlatinumOrTrialLicense, + } = await getCapabilities(); + expect(upgradeInProgress).toBe(false); expect(mlFeatureEnabledInSpace).toBe(false); + expect(isPlatinumOrTrialLicense).toBe(false); + + expect(capabilities.canAccessML).toBe(false); expect(capabilities.canGetJobs).toBe(false); + expect(capabilities.canGetDatafeeds).toBe(false); + expect(capabilities.canGetCalendars).toBe(false); + expect(capabilities.canFindFileStructure).toBe(false); + expect(capabilities.canGetFilters).toBe(false); + expect(capabilities.canGetDataFrameAnalytics).toBe(false); + expect(capabilities.canGetAnnotations).toBe(false); + expect(capabilities.canCreateAnnotation).toBe(false); + expect(capabilities.canDeleteAnnotation).toBe(false); + expect(capabilities.canCreateJob).toBe(false); expect(capabilities.canDeleteJob).toBe(false); expect(capabilities.canOpenJob).toBe(false); expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canCreateDatafeed).toBe(false); + expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canCreateFilter).toBe(false); expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(false); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); expect(capabilities.canCreateDataFrameAnalytics).toBe(false); expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts index a2ad83c5522de3..d955cf981faca6 100644 --- a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts @@ -44,4 +44,6 @@ function disableAdminPrivileges(capabilities: MlCapabilities) { Object.keys(adminMlCapabilities).forEach(k => { capabilities[k as keyof MlCapabilities] = false; }); + capabilities.canCreateAnnotation = false; + capabilities.canDeleteAnnotation = false; } diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 64f8eb4b0acd3a..969b74194148b3 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -45,7 +45,7 @@ import { systemRoutes } from './routes/system'; import { MlLicense } from '../common/license'; import { MlServerLicense } from './lib/license'; import { createSharedServices, SharedServices } from './shared_services'; -import { userMlCapabilities, adminMlCapabilities } from '../common/types/capabilities'; +import { getPluginPrivileges } from '../common/types/capabilities'; import { setupCapabilitiesSwitcher } from './lib/capabilities'; import { registerKibanaSettings } from './lib/register_settings'; @@ -75,8 +75,7 @@ export class MlServerPlugin implements Plugin { try { @@ -86,6 +89,9 @@ export function annotationRoutes( validate: { body: indexAnnotationSchema, }, + options: { + tags: ['access:ml:canCreateAnnotation'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -130,6 +136,9 @@ export function annotationRoutes( validate: { params: deleteAnnotationSchema, }, + options: { + tags: ['access:ml:canDeleteAnnotation'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts index ca63d69f403f6b..63cd5498231af5 100644 --- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -37,6 +37,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/anomaly_detectors', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -65,6 +68,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: jobIdSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -93,6 +99,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/anomaly_detectors/_stats', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -121,6 +130,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: jobIdSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -154,6 +166,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { params: jobIdSchema, body: schema.object(anomalyDetectionJobSchema), }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -188,6 +203,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { params: jobIdSchema, body: anomalyDetectionUpdateJobSchema, }, + options: { + tags: ['access:ml:canUpdateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -220,6 +238,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: jobIdSchema, }, + options: { + tags: ['access:ml:canOpenJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -251,6 +272,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: jobIdSchema, }, + options: { + tags: ['access:ml:canCloseJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -286,6 +310,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: jobIdSchema, }, + options: { + tags: ['access:ml:canDeleteJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -319,6 +346,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: schema.any(), }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -351,6 +381,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { params: jobIdSchema, body: forecastAnomalyDetector, }, + options: { + tags: ['access:ml:canForecastJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -389,6 +422,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { params: jobIdSchema, body: getRecordsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -425,6 +461,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { params: getBucketParamsSchema, body: getBucketsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -462,6 +501,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { params: jobIdSchema, body: getOverallBucketsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -496,6 +538,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: getCategoriesSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/calendars.ts b/x-pack/plugins/ml/server/routes/calendars.ts index a17601f74ae93e..9c80651a13999e 100644 --- a/x-pack/plugins/ml/server/routes/calendars.ts +++ b/x-pack/plugins/ml/server/routes/calendars.ts @@ -52,6 +52,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/calendars', validate: false, + options: { + tags: ['access:ml:canGetCalendars'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -81,6 +84,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { validate: { params: calendarIdsSchema, }, + options: { + tags: ['access:ml:canGetCalendars'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { let returnValue; @@ -117,6 +123,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { validate: { body: calendarSchema, }, + options: { + tags: ['access:ml:canCreateCalendar'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -149,6 +158,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { params: calendarIdSchema, body: calendarSchema, }, + options: { + tags: ['access:ml:canCreateCalendar'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -180,6 +192,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { validate: { params: calendarIdSchema, }, + options: { + tags: ['access:ml:canDeleteCalendar'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index dd9e0ea66aa9dd..32cb2b343f8760 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -33,6 +33,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat { path: '/api/ml/data_frame/analytics', validate: false, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -61,6 +64,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { params: analyticsIdSchema, }, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -88,6 +94,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat { path: '/api/ml/data_frame/analytics/_stats', validate: false, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -118,6 +127,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { params: analyticsIdSchema, }, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -155,6 +167,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat params: analyticsIdSchema, body: dataAnalyticsJobConfigSchema, }, + options: { + tags: ['access:ml:canCreateDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -190,6 +205,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { body: dataAnalyticsEvaluateSchema, }, + options: { + tags: ['access:ml:canCreateDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -224,6 +242,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { body: dataAnalyticsExplainSchema, }, + options: { + tags: ['access:ml:canCreateDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -257,6 +278,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { params: analyticsIdSchema, }, + options: { + tags: ['access:ml:canDeleteDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -291,6 +315,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { params: analyticsIdSchema, }, + options: { + tags: ['access:ml:canStartStopDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -324,6 +351,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat params: analyticsIdSchema, query: stopsDataFrameAnalyticsJobQuerySchema, }, + options: { + tags: ['access:ml:canStartStopDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -364,6 +394,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { params: analyticsIdSchema, }, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/data_visualizer.ts b/x-pack/plugins/ml/server/routes/data_visualizer.ts index 20029fbd8d1a6b..04008a896a1a22 100644 --- a/x-pack/plugins/ml/server/routes/data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/data_visualizer.ts @@ -88,6 +88,9 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) params: indexPatternTitleSchema, body: dataVisualizerFieldStatsSchema, }, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { @@ -150,6 +153,9 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) params: indexPatternTitleSchema, body: dataVisualizerOverallStatsSchema, }, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/datafeeds.ts b/x-pack/plugins/ml/server/routes/datafeeds.ts index ec667e1d305f52..1fa1d408372da3 100644 --- a/x-pack/plugins/ml/server/routes/datafeeds.ts +++ b/x-pack/plugins/ml/server/routes/datafeeds.ts @@ -28,6 +28,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/datafeeds', validate: false, + options: { + tags: ['access:ml:canGetDatafeeds'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -57,6 +60,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: datafeedIdSchema, }, + options: { + tags: ['access:ml:canGetDatafeeds'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -83,6 +89,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/datafeeds/_stats', validate: false, + options: { + tags: ['access:ml:canGetDatafeeds'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -112,6 +121,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: datafeedIdSchema, }, + options: { + tags: ['access:ml:canGetDatafeeds'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -146,6 +158,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { params: datafeedIdSchema, body: datafeedConfigSchema, }, + options: { + tags: ['access:ml:canCreateDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -181,6 +196,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { params: datafeedIdSchema, body: datafeedConfigSchema, }, + options: { + tags: ['access:ml:canUpdateDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -216,6 +234,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { params: datafeedIdSchema, query: deleteDatafeedQuerySchema, }, + options: { + tags: ['access:ml:canDeleteDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -255,6 +276,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { params: datafeedIdSchema, body: startDatafeedSchema, }, + options: { + tags: ['access:ml:canStartStopDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -291,6 +315,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: datafeedIdSchema, }, + options: { + tags: ['access:ml:canStartStopDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -324,6 +351,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: datafeedIdSchema, }, + options: { + tags: ['access:ml:canPreviewDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/fields_service.ts b/x-pack/plugins/ml/server/routes/fields_service.ts index 577e8e0161342a..b0f13df294145e 100644 --- a/x-pack/plugins/ml/server/routes/fields_service.ts +++ b/x-pack/plugins/ml/server/routes/fields_service.ts @@ -46,8 +46,10 @@ export function fieldsService({ router, mlLicense }: RouteInitialization) { validate: { body: getCardinalityOfFieldsSchema, }, + options: { + tags: ['access:ml:canAccessML'], + }, }, - mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const resp = await getCardinalityOfFields(context, request.body); @@ -79,6 +81,9 @@ export function fieldsService({ router, mlLicense }: RouteInitialization) { validate: { body: getTimeFieldRangeSchema, }, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts index 3f3fc3f547b6a0..0f389f9505943b 100644 --- a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts @@ -71,6 +71,7 @@ export function fileDataVisualizerRoutes({ router, mlLicense }: RouteInitializat accepts: ['text/*', 'application/json'], maxBytes: MAX_FILE_SIZE_BYTES, }, + tags: ['access:ml:canFindFileStructure'], }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { @@ -105,6 +106,7 @@ export function fileDataVisualizerRoutes({ router, mlLicense }: RouteInitializat accepts: ['application/json'], maxBytes: MAX_FILE_SIZE_BYTES, }, + tags: ['access:ml:canFindFileStructure'], }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { diff --git a/x-pack/plugins/ml/server/routes/filters.ts b/x-pack/plugins/ml/server/routes/filters.ts index 738c25070358d1..d5287c349a8fca 100644 --- a/x-pack/plugins/ml/server/routes/filters.ts +++ b/x-pack/plugins/ml/server/routes/filters.ts @@ -57,6 +57,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/filters', validate: false, + options: { + tags: ['access:ml:canGetFilters'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -89,6 +92,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: filterIdSchema, }, + options: { + tags: ['access:ml:canGetFilters'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -120,6 +126,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: createFilterSchema, }, + options: { + tags: ['access:ml:canCreateFilter'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -155,6 +164,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { params: filterIdSchema, body: updateFilterSchema, }, + options: { + tags: ['access:ml:canCreateFilter'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -186,6 +198,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: filterIdSchema, }, + options: { + tags: ['access:ml:canDeleteFilter'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -216,6 +231,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/filters/_stats', validate: false, + options: { + tags: ['access:ml:canGetFilters'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/indices.ts b/x-pack/plugins/ml/server/routes/indices.ts index e434936beba630..fb3ef7fc41c767 100644 --- a/x-pack/plugins/ml/server/routes/indices.ts +++ b/x-pack/plugins/ml/server/routes/indices.ts @@ -27,6 +27,9 @@ export function indicesRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: indicesSchema, }, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/job_audit_messages.ts b/x-pack/plugins/ml/server/routes/job_audit_messages.ts index 1fe5a7af95d4f0..5acc89e7d13be7 100644 --- a/x-pack/plugins/ml/server/routes/job_audit_messages.ts +++ b/x-pack/plugins/ml/server/routes/job_audit_messages.ts @@ -33,6 +33,9 @@ export function jobAuditMessagesRoutes({ router, mlLicense }: RouteInitializatio params: jobAuditMessagesJobIdSchema, query: jobAuditMessagesQuerySchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -67,6 +70,9 @@ export function jobAuditMessagesRoutes({ router, mlLicense }: RouteInitializatio validate: { query: jobAuditMessagesQuerySchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts index 149ca2591fd765..05c44e1da97575 100644 --- a/x-pack/plugins/ml/server/routes/job_service.ts +++ b/x-pack/plugins/ml/server/routes/job_service.ts @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; import { schema } from '@kbn/config-schema'; -import { KibanaRequest } from 'kibana/server'; import { wrapError } from '../client/error_wrapper'; -import { RouteInitialization, JobServiceRouteDeps } from '../types'; +import { RouteInitialization } from '../types'; import { categorizationFieldExamplesSchema, chartSchema, @@ -27,19 +25,7 @@ import { categorizationExamplesProvider } from '../models/job_service/new_job'; /** * Routes for job service */ -export function jobServiceRoutes( - { router, mlLicense }: RouteInitialization, - { resolveMlCapabilities }: JobServiceRouteDeps -) { - async function hasPermissionToCreateJobs(request: KibanaRequest) { - const mlCapabilities = await resolveMlCapabilities(request); - if (mlCapabilities === null) { - throw new Error('resolveMlCapabilities is not defined'); - } - - return mlCapabilities.canCreateJob; - } - +export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { /** * @apiGroup JobService * @@ -55,6 +41,9 @@ export function jobServiceRoutes( validate: { body: forceStartDatafeedSchema, }, + options: { + tags: ['access:ml:canStartStopDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -86,6 +75,9 @@ export function jobServiceRoutes( validate: { body: datafeedIdsSchema, }, + options: { + tags: ['access:ml:canStartStopDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -117,6 +109,9 @@ export function jobServiceRoutes( validate: { body: jobIdsSchema, }, + options: { + tags: ['access:ml:canDeleteJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -148,6 +143,9 @@ export function jobServiceRoutes( validate: { body: jobIdsSchema, }, + options: { + tags: ['access:ml:canCloseJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -184,6 +182,9 @@ export function jobServiceRoutes( validate: { body: jobIdsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -215,6 +216,9 @@ export function jobServiceRoutes( validate: { body: schema.object(jobsWithTimerangeSchema), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -245,6 +249,9 @@ export function jobServiceRoutes( validate: { body: jobIdsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -272,6 +279,9 @@ export function jobServiceRoutes( { path: '/api/ml/jobs/groups', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -302,6 +312,9 @@ export function jobServiceRoutes( validate: { body: schema.object(updateGroupsSchema), }, + options: { + tags: ['access:ml:canUpdateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -329,6 +342,9 @@ export function jobServiceRoutes( { path: '/api/ml/jobs/deleting_jobs_tasks', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -359,6 +375,9 @@ export function jobServiceRoutes( validate: { body: jobIdsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -389,6 +408,9 @@ export function jobServiceRoutes( params: schema.object({ indexPattern: schema.string() }), query: schema.maybe(schema.object({ rollup: schema.maybe(schema.string()) })), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -422,6 +444,9 @@ export function jobServiceRoutes( validate: { body: schema.object(chartSchema), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -474,6 +499,9 @@ export function jobServiceRoutes( validate: { body: schema.object(chartSchema), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -522,6 +550,9 @@ export function jobServiceRoutes( { path: '/api/ml/jobs/all_jobs_and_group_ids', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -552,6 +583,9 @@ export function jobServiceRoutes( validate: { body: schema.object(lookBackProgressSchema), }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -583,17 +617,12 @@ export function jobServiceRoutes( validate: { body: schema.object(categorizationFieldExamplesSchema), }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { - // due to the use of the _analyze endpoint which is called by the kibana user, - // basic job creation privileges are required to use this endpoint - if ((await hasPermissionToCreateJobs(request)) === false) { - throw Boom.forbidden( - 'Insufficient privileges, the machine_learning_admin role is required.' - ); - } - const { validateCategoryExamples } = categorizationExamplesProvider( context.ml!.mlClient.callAsCurrentUser, context.ml!.mlClient.callAsInternalUser @@ -644,6 +673,9 @@ export function jobServiceRoutes( validate: { body: schema.object(topCategoriesSchema), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/job_validation.ts b/x-pack/plugins/ml/server/routes/job_validation.ts index dd2bd9deadf43f..632166d6d5fb8f 100644 --- a/x-pack/plugins/ml/server/routes/job_validation.ts +++ b/x-pack/plugins/ml/server/routes/job_validation.ts @@ -57,6 +57,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, validate: { body: estimateBucketSpanSchema, }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -106,6 +109,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, validate: { body: modelMemoryLimitSchema, }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -135,6 +141,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, validate: { body: validateCardinalitySchema, }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -167,6 +176,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, validate: { body: validateJobSchema, }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/modules.ts b/x-pack/plugins/ml/server/routes/modules.ts index 2891144fc4574a..622ae66ede4264 100644 --- a/x-pack/plugins/ml/server/routes/modules.ts +++ b/x-pack/plugins/ml/server/routes/modules.ts @@ -97,6 +97,9 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { indexPatternTitle: schema.string(), }), }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -127,6 +130,9 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { ...getModuleIdParamSchema(true), }), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -161,6 +167,9 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { params: schema.object(getModuleIdParamSchema()), body: setupModuleBodySchema, }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -218,6 +227,9 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { validate: { params: schema.object(getModuleIdParamSchema()), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/notification_settings.ts b/x-pack/plugins/ml/server/routes/notification_settings.ts index 59458b1e486db2..e4a9abb0784beb 100644 --- a/x-pack/plugins/ml/server/routes/notification_settings.ts +++ b/x-pack/plugins/ml/server/routes/notification_settings.ts @@ -22,6 +22,9 @@ export function notificationRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/notification_settings', validate: false, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/results_service.ts b/x-pack/plugins/ml/server/routes/results_service.ts index 89c267340fe52f..94ca0827ccfa59 100644 --- a/x-pack/plugins/ml/server/routes/results_service.ts +++ b/x-pack/plugins/ml/server/routes/results_service.ts @@ -88,6 +88,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) validate: { body: anomaliesTableDataSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -117,6 +120,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) validate: { body: categoryDefinitionSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -146,6 +152,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) validate: { body: maxAnomalyScoreSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -175,6 +184,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) validate: { body: categoryExamplesSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -204,6 +216,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) validate: { body: partitionFieldValuesSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/system.ts b/x-pack/plugins/ml/server/routes/system.ts index d5fe45728c56c7..7ae7dd8eef0653 100644 --- a/x-pack/plugins/ml/server/routes/system.ts +++ b/x-pack/plugins/ml/server/routes/system.ts @@ -54,6 +54,9 @@ export function systemRoutes( validate: { body: schema.maybe(schema.any()), }, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { @@ -110,6 +113,9 @@ export function systemRoutes( { path: '/api/ml/ml_capabilities', validate: false, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { @@ -150,7 +156,11 @@ export function systemRoutes( { path: '/api/ml/ml_node_count', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, + mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { // check for basic license first for consistency with other @@ -201,6 +211,9 @@ export function systemRoutes( { path: '/api/ml/info', validate: false, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { @@ -229,6 +242,9 @@ export function systemRoutes( validate: { body: schema.maybe(schema.any()), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/types.ts b/x-pack/plugins/ml/server/types.ts index d4cd61a7fa4f71..678e81d3526ac8 100644 --- a/x-pack/plugins/ml/server/types.ts +++ b/x-pack/plugins/ml/server/types.ts @@ -30,10 +30,6 @@ export interface SystemRouteDeps { resolveMlCapabilities: ResolveMlCapabilities; } -export interface JobServiceRouteDeps { - resolveMlCapabilities: ResolveMlCapabilities; -} - export interface PluginsSetup { cloud: CloudSetup; features: FeaturesPluginSetup; diff --git a/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts b/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts index bbc766df34dcfe..10857caab98e27 100644 --- a/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts +++ b/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts @@ -91,12 +91,11 @@ export default ({ getService }: FtrProviderContext) => { model_plot_config: { enabled: true }, }, expected: { - responseCode: 403, + responseCode: 404, responseBody: { - statusCode: 403, - error: 'Forbidden', - message: - '[security_exception] action [cluster:admin/xpack/ml/job/put] is unauthorized for user [ml_viewer]', + statusCode: 404, + error: 'Not Found', + message: 'Not Found', }, }, }, diff --git a/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts b/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts index 6a57db1687868f..a5cb68d7821267 100644 --- a/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts +++ b/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts @@ -197,8 +197,8 @@ export default ({ getService }: FtrProviderContext) => { requestBody: {}, // Note that the jobs and datafeeds are loaded async so the actual error message is not deterministic. expected: { - responseCode: 403, - error: 'Forbidden', + responseCode: 404, + error: 'Not Found', }, }, ]; diff --git a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts index 23ddd3b63a2eff..c42fc95c1bc7f2 100644 --- a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts @@ -84,10 +84,9 @@ export default ({ getService }: FtrProviderContext) => { startDatafeed: false, }, expected: { - responseCode: 403, - error: 'Forbidden', - message: - '[security_exception] action [cluster:monitor/xpack/ml/info/get] is unauthorized for user [ml_unauthorized]', + responseCode: 404, + error: 'Not Found', + message: 'Not Found', }, }, ]; From 1512859e56d5196665ffbfaefd9b7a25d8b24100 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Wed, 29 Apr 2020 10:54:19 -0700 Subject: [PATCH 04/30] [Telemetry] oss api tests (#64602) * Adds telemetry API tests for oss * Modifies test expectations to match that within oss Co-authored-by: Elastic Machine --- test/api_integration/apis/index.js | 1 + test/api_integration/apis/telemetry/index.js | 26 ++++ test/api_integration/apis/telemetry/opt_in.ts | 123 ++++++++++++++++ .../apis/telemetry/telemetry_local.js | 133 ++++++++++++++++++ .../telemetry/telemetry_optin_notice_seen.ts | 59 ++++++++ 5 files changed, 342 insertions(+) create mode 100644 test/api_integration/apis/telemetry/index.js create mode 100644 test/api_integration/apis/telemetry/opt_in.ts create mode 100644 test/api_integration/apis/telemetry/telemetry_local.js create mode 100644 test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts diff --git a/test/api_integration/apis/index.js b/test/api_integration/apis/index.js index c5bfc847d0041f..0c4028905657d5 100644 --- a/test/api_integration/apis/index.js +++ b/test/api_integration/apis/index.js @@ -33,5 +33,6 @@ export default function({ loadTestFile }) { loadTestFile(require.resolve('./status')); loadTestFile(require.resolve('./stats')); loadTestFile(require.resolve('./ui_metric')); + loadTestFile(require.resolve('./telemetry')); }); } diff --git a/test/api_integration/apis/telemetry/index.js b/test/api_integration/apis/telemetry/index.js new file mode 100644 index 00000000000000..c79f5cb4708903 --- /dev/null +++ b/test/api_integration/apis/telemetry/index.js @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export default function({ loadTestFile }) { + describe('Telemetry', () => { + loadTestFile(require.resolve('./telemetry_local')); + loadTestFile(require.resolve('./opt_in')); + loadTestFile(require.resolve('./telemetry_optin_notice_seen')); + }); +} diff --git a/test/api_integration/apis/telemetry/opt_in.ts b/test/api_integration/apis/telemetry/opt_in.ts new file mode 100644 index 00000000000000..e4654ee3985f32 --- /dev/null +++ b/test/api_integration/apis/telemetry/opt_in.ts @@ -0,0 +1,123 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; + +import { TelemetrySavedObjectAttributes } from 'src/plugins/telemetry/server/telemetry_repository'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function optInTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + describe('/api/telemetry/v2/optIn API', () => { + let defaultAttributes: TelemetrySavedObjectAttributes; + let kibanaVersion: any; + before(async () => { + const kibanaVersionAccessor = kibanaServer.version; + kibanaVersion = await kibanaVersionAccessor.get(); + defaultAttributes = + (await getSavedObjectAttributes(supertest).catch(err => { + if (err.message === 'expected 200 "OK", got 404 "Not Found"') { + return null; + } + throw err; + })) || {}; + + expect(typeof kibanaVersion).to.eql('string'); + expect(kibanaVersion.length).to.be.greaterThan(0); + }); + + afterEach(async () => { + await updateSavedObjectAttributes(supertest, defaultAttributes); + }); + + it('should support sending false with allowChangingOptInStatus true', async () => { + await updateSavedObjectAttributes(supertest, { + ...defaultAttributes, + allowChangingOptInStatus: true, + }); + await postTelemetryV2Optin(supertest, false, 200); + const { enabled, lastVersionChecked } = await getSavedObjectAttributes(supertest); + expect(enabled).to.be(false); + expect(lastVersionChecked).to.be(kibanaVersion); + }); + + it('should support sending true with allowChangingOptInStatus true', async () => { + await updateSavedObjectAttributes(supertest, { + ...defaultAttributes, + allowChangingOptInStatus: true, + }); + await postTelemetryV2Optin(supertest, true, 200); + const { enabled, lastVersionChecked } = await getSavedObjectAttributes(supertest); + expect(enabled).to.be(true); + expect(lastVersionChecked).to.be(kibanaVersion); + }); + + it('should not support sending false with allowChangingOptInStatus false', async () => { + await updateSavedObjectAttributes(supertest, { + ...defaultAttributes, + allowChangingOptInStatus: false, + }); + await postTelemetryV2Optin(supertest, false, 400); + }); + + it('should not support sending true with allowChangingOptInStatus false', async () => { + await updateSavedObjectAttributes(supertest, { + ...defaultAttributes, + allowChangingOptInStatus: false, + }); + await postTelemetryV2Optin(supertest, true, 400); + }); + + it('should not support sending null', async () => { + await postTelemetryV2Optin(supertest, null, 400); + }); + + it('should not support sending junk', async () => { + await postTelemetryV2Optin(supertest, 42, 400); + }); + }); +} + +async function postTelemetryV2Optin(supertest: any, value: any, statusCode: number): Promise { + const { body } = await supertest + .post('/api/telemetry/v2/optIn') + .set('kbn-xsrf', 'xxx') + .send({ enabled: value }) + .expect(statusCode); + + return body; +} + +async function updateSavedObjectAttributes( + supertest: any, + attributes: TelemetrySavedObjectAttributes +): Promise { + return await supertest + .post('/api/saved_objects/telemetry/telemetry') + .query({ overwrite: true }) + .set('kbn-xsrf', 'xxx') + .send({ attributes }) + .expect(200); +} + +async function getSavedObjectAttributes(supertest: any): Promise { + const { body } = await supertest.get('/api/saved_objects/telemetry/telemetry').expect(200); + return body.attributes; +} diff --git a/test/api_integration/apis/telemetry/telemetry_local.js b/test/api_integration/apis/telemetry/telemetry_local.js new file mode 100644 index 00000000000000..84bfd8a755c116 --- /dev/null +++ b/test/api_integration/apis/telemetry/telemetry_local.js @@ -0,0 +1,133 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import _ from 'lodash'; + +/* + * Create a single-level array with strings for all the paths to values in the + * source object, up to 3 deep. Going deeper than 3 causes a bit too much churn + * in the tests. + */ +function flatKeys(source) { + const recursivelyFlatKeys = (obj, path = [], depth = 0) => { + return depth < 3 && _.isObject(obj) + ? _.map(obj, (v, k) => recursivelyFlatKeys(v, [...path, k], depth + 1)) + : path.join('.'); + }; + + return _.uniq(_.flattenDeep(recursivelyFlatKeys(source))).sort((a, b) => a.localeCompare(b)); +} + +export default function({ getService }) { + const supertest = getService('supertest'); + + describe('/api/telemetry/v2/clusters/_stats', () => { + it('should pull local stats and validate data types', async () => { + const timeRange = { + min: '2018-07-23T22:07:00Z', + max: '2018-07-23T22:13:00Z', + }; + + const { body } = await supertest + .post('/api/telemetry/v2/clusters/_stats') + .set('kbn-xsrf', 'xxx') + .send({ timeRange, unencrypted: true }) + .expect(200); + + expect(body.length).to.be(1); + const stats = body[0]; + expect(stats.collection).to.be('local'); + expect(stats.stack_stats.kibana.count).to.be.a('number'); + expect(stats.stack_stats.kibana.indices).to.be.a('number'); + expect(stats.stack_stats.kibana.os.platforms[0].platform).to.be.a('string'); + expect(stats.stack_stats.kibana.os.platforms[0].count).to.be(1); + expect(stats.stack_stats.kibana.os.platformReleases[0].platformRelease).to.be.a('string'); + expect(stats.stack_stats.kibana.os.platformReleases[0].count).to.be(1); + expect(stats.stack_stats.kibana.plugins.telemetry.opt_in_status).to.be(false); + expect(stats.stack_stats.kibana.plugins.telemetry.usage_fetcher).to.be.a('string'); + expect(stats.stack_stats.kibana.plugins.stack_management).to.be.an('object'); + expect(stats.stack_stats.kibana.plugins.ui_metric).to.be.an('object'); + expect(stats.stack_stats.kibana.plugins.application_usage).to.be.an('object'); + expect(stats.stack_stats.kibana.plugins.kql.defaultQueryLanguage).to.be.a('string'); + expect(stats.stack_stats.kibana.plugins['tsvb-validation']).to.be.an('object'); + expect(stats.stack_stats.kibana.plugins.localization).to.be.an('object'); + expect(stats.stack_stats.kibana.plugins.csp.strict).to.be(true); + expect(stats.stack_stats.kibana.plugins.csp.warnLegacyBrowsers).to.be(true); + expect(stats.stack_stats.kibana.plugins.csp.rulesChangedFromDefault).to.be(false); + }); + + it('should pull local stats and validate fields', async () => { + const timeRange = { + min: '2018-07-23T22:07:00Z', + max: '2018-07-23T22:13:00Z', + }; + + const { body } = await supertest + .post('/api/telemetry/v2/clusters/_stats') + .set('kbn-xsrf', 'xxx') + .send({ timeRange, unencrypted: true }) + .expect(200); + + const stats = body[0]; + + const actual = flatKeys(stats); + expect(actual).to.be.an('array'); + const expected = [ + 'cluster_name', + 'cluster_stats.cluster_uuid', + 'cluster_stats.indices.analysis', + 'cluster_stats.indices.completion', + 'cluster_stats.indices.count', + 'cluster_stats.indices.docs', + 'cluster_stats.indices.fielddata', + 'cluster_stats.indices.mappings', + 'cluster_stats.indices.query_cache', + 'cluster_stats.indices.segments', + 'cluster_stats.indices.shards', + 'cluster_stats.indices.store', + 'cluster_stats.nodes.count', + 'cluster_stats.nodes.discovery_types', + 'cluster_stats.nodes.fs', + 'cluster_stats.nodes.ingest', + 'cluster_stats.nodes.jvm', + 'cluster_stats.nodes.network_types', + 'cluster_stats.nodes.os', + 'cluster_stats.nodes.packaging_types', + 'cluster_stats.nodes.plugins', + 'cluster_stats.nodes.process', + 'cluster_stats.nodes.versions', + 'cluster_stats.status', + 'cluster_stats.timestamp', + 'cluster_uuid', + 'collection', + 'collectionSource', + 'stack_stats.kibana.count', + 'stack_stats.kibana.indices', + 'stack_stats.kibana.os', + 'stack_stats.kibana.plugins', + 'stack_stats.kibana.versions', + 'timestamp', + 'version', + ]; + + expect(expected.every(m => actual.includes(m))).to.be.ok(); + }); + }); +} diff --git a/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts b/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts new file mode 100644 index 00000000000000..4413c672fb46c3 --- /dev/null +++ b/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { Client, DeleteDocumentParams, GetParams, GetResponse } from 'elasticsearch'; +import { TelemetrySavedObjectAttributes } from 'src/plugins/telemetry/server/telemetry_repository'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function optInTest({ getService }: FtrProviderContext) { + const client: Client = getService('legacyEs'); + const supertest = getService('supertest'); + + describe('/api/telemetry/v2/userHasSeenNotice API Telemetry User has seen OptIn Notice', () => { + it('should update telemetry setting field via PUT', async () => { + try { + await client.delete({ + index: '.kibana', + id: 'telemetry:telemetry', + } as DeleteDocumentParams); + } catch (err) { + if (err.statusCode !== 404) { + throw err; + } + } + + await supertest + .put('/api/telemetry/v2/userHasSeenNotice') + .set('kbn-xsrf', 'xxx') + .expect(200); + + const { + _source: { telemetry }, + }: GetResponse<{ + telemetry: TelemetrySavedObjectAttributes; + }> = await client.get({ + index: '.kibana', + id: 'telemetry:telemetry', + } as GetParams); + + expect(telemetry.userHasSeenNotice).to.be(true); + }); + }); +} From 3adab851380f4a368b9aa14cb227ef9f61e16e93 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 29 Apr 2020 13:01:23 -0500 Subject: [PATCH 05/30] Minimize dependencies required by our telemetry middleware (#64665) 1. we need our redux actions for our telemetry middleware, which: 2. require types from an index file, which: 3. includes all of our model types, and everything involved in them By moving these types to a separate file and importing _that_instead, we bypass inclusion of 2 and 3 in our plugin, which equates to ~550kB (of a total of ~600kB). --- .../siem/public/lib/telemetry/middleware.ts | 2 +- x-pack/plugins/siem/public/store/model.ts | 13 +------------ .../siem/public/store/timeline/actions.ts | 2 +- x-pack/plugins/siem/public/store/types.ts | 17 +++++++++++++++++ 4 files changed, 20 insertions(+), 14 deletions(-) create mode 100644 x-pack/plugins/siem/public/store/types.ts diff --git a/x-pack/plugins/siem/public/lib/telemetry/middleware.ts b/x-pack/plugins/siem/public/lib/telemetry/middleware.ts index 59c6cb35669070..ca889e20e695f3 100644 --- a/x-pack/plugins/siem/public/lib/telemetry/middleware.ts +++ b/x-pack/plugins/siem/public/lib/telemetry/middleware.ts @@ -7,7 +7,7 @@ import { Action, Dispatch, MiddlewareAPI } from 'redux'; import { track, METRIC_TYPE, TELEMETRY_EVENT } from './'; -import { timelineActions } from '../../store/actions'; +import * as timelineActions from '../../store/timeline/actions'; export const telemetryMiddleware = (api: MiddlewareAPI) => (next: Dispatch) => (action: Action) => { if (timelineActions.endTimelineSaving.match(action)) { diff --git a/x-pack/plugins/siem/public/store/model.ts b/x-pack/plugins/siem/public/store/model.ts index 9e9e663a59fe09..686dc096e61b01 100644 --- a/x-pack/plugins/siem/public/store/model.ts +++ b/x-pack/plugins/siem/public/store/model.ts @@ -9,15 +9,4 @@ export { dragAndDropModel } from './drag_and_drop'; export { hostsModel } from './hosts'; export { inputsModel } from './inputs'; export { networkModel } from './network'; - -export type KueryFilterQueryKind = 'kuery' | 'lucene'; - -export interface KueryFilterQuery { - kind: KueryFilterQueryKind; - expression: string; -} - -export interface SerializedFilterQuery { - kuery: KueryFilterQuery | null; - serializedQuery: string; -} +export * from './types'; diff --git a/x-pack/plugins/siem/public/store/timeline/actions.ts b/x-pack/plugins/siem/public/store/timeline/actions.ts index a03cc2643e014a..12155decf40d44 100644 --- a/x-pack/plugins/siem/public/store/timeline/actions.ts +++ b/x-pack/plugins/siem/public/store/timeline/actions.ts @@ -12,7 +12,7 @@ import { DataProvider, QueryOperator, } from '../../components/timeline/data_providers/data_provider'; -import { KueryFilterQuery, SerializedFilterQuery } from '../model'; +import { KueryFilterQuery, SerializedFilterQuery } from '../types'; import { EventType, KqlMode, TimelineModel, ColumnHeaderOptions } from './model'; import { TimelineNonEcsData } from '../../graphql/types'; diff --git a/x-pack/plugins/siem/public/store/types.ts b/x-pack/plugins/siem/public/store/types.ts new file mode 100644 index 00000000000000..2c679ba41116ee --- /dev/null +++ b/x-pack/plugins/siem/public/store/types.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type KueryFilterQueryKind = 'kuery' | 'lucene'; + +export interface KueryFilterQuery { + kind: KueryFilterQueryKind; + expression: string; +} + +export interface SerializedFilterQuery { + kuery: KueryFilterQuery | null; + serializedQuery: string; +} From 53ff22997a1401ccbd2808831b26b92fa174de92 Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Wed, 29 Apr 2020 14:06:08 -0400 Subject: [PATCH 06/30] [EPM] Update UI to handle package versions and updates (#64689) * link to installed version of detail page * add latestVersion property to EPM get package endpoint * add updates available notices * add update package button * handle various states and send installedVersion from package endpoint * fix type errors * fix install error because not returning promises * track version in state * handle unsuccessful update attempt * remove unused variable --- .../common/services/package_to_config.test.ts | 1 + .../ingest_manager/common/types/models/epm.ts | 2 + .../sections/epm/components/icons.tsx | 15 ++ .../sections/epm/components/package_card.tsx | 8 +- .../epm/hooks/use_package_install.tsx | 63 +++-- .../sections/epm/screens/detail/content.tsx | 3 +- .../epm/screens/detail/data_sources_panel.tsx | 2 +- .../sections/epm/screens/detail/header.tsx | 17 +- .../sections/epm/screens/detail/index.tsx | 4 +- .../screens/detail/installation_button.tsx | 23 +- .../epm/screens/detail/settings_panel.tsx | 236 ++++++++++++------ .../epm/screens/detail/side_nav_links.tsx | 2 +- .../server/services/epm/packages/get.ts | 4 +- .../server/services/epm/packages/index.ts | 1 + .../server/services/epm/packages/install.ts | 2 +- .../server/services/epm/packages/remove.ts | 8 +- 16 files changed, 277 insertions(+), 114 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx diff --git a/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts b/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts index cb290e61b17e51..a977a1a66e059a 100644 --- a/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts @@ -11,6 +11,7 @@ describe('Ingest Manager - packageToConfig', () => { name: 'mock-package', title: 'Mock package', version: '0.0.0', + latestVersion: '0.0.0', description: 'description', type: 'mock', categories: [], diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index 05e160cdfb81ac..f8779a879a049f 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -204,6 +204,8 @@ export interface RegistryVarsEntry { // internal until we need them interface PackageAdditions { title: string; + latestVersion: string; + installedVersion?: string; assets: AssetsGroupedByServiceByType; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx new file mode 100644 index 00000000000000..64223efefaab8c --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiIcon } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +export const StyledAlert = styled(EuiIcon)` + color: ${props => props.theme.eui.euiColorWarning}; + padding: 0 5px; +`; + +export const UpdateIcon = () => ; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx index 8ad081cbbabe40..ab7e87b3ad06cb 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx @@ -30,9 +30,15 @@ export function PackageCard({ showInstalledBadge, status, icons, + ...restProps }: PackageCardProps) { const { toDetailView } = useLinks(); - const url = toDetailView({ name, version }); + let urlVersion = version; + // if this is an installed package, link to the version installed + if ('savedObject' in restProps) { + urlVersion = restProps.savedObject.attributes.version || version; + } + const url = toDetailView({ name, version: urlVersion }); return ( ; +type InstallPackageProps = Pick & { + fromUpdate?: boolean; +}; +type SetPackageInstallStatusProps = Pick & PackageInstallItem; function usePackageInstall({ notifications }: { notifications: NotificationsStart }) { + const { toDetailView } = useLinks(); const [packages, setPackage] = useState({}); const setPackageInstallStatus = useCallback( - ({ name, status }: { name: PackageInfo['name']; status: InstallStatus }) => { + ({ name, status, version }: SetPackageInstallStatusProps) => { + const packageProps: PackageInstallItem = { + status, + version, + }; setPackage((prev: PackagesInstall) => ({ ...prev, - [name]: { status }, + [name]: packageProps, })); }, [] ); + const getPackageInstallStatus = useCallback( + (pkg: string): PackageInstallItem => { + return packages[pkg]; + }, + [packages] + ); + const installPackage = useCallback( - async ({ name, version, title }: InstallPackageProps) => { - setPackageInstallStatus({ name, status: InstallStatus.installing }); + async ({ name, version, title, fromUpdate = false }: InstallPackageProps) => { + const currStatus = getPackageInstallStatus(name); + const newStatus = { ...currStatus, name, status: InstallStatus.installing }; + setPackageInstallStatus(newStatus); const pkgkey = `${name}-${version}`; const res = await sendInstallPackage(pkgkey); if (res.error) { - setPackageInstallStatus({ name, status: InstallStatus.notInstalled }); + if (fromUpdate) { + // if there is an error during update, set it back to the previous version + // as handling of bad update is not implemented yet + setPackageInstallStatus({ ...currStatus, name }); + } else { + setPackageInstallStatus({ name, status: InstallStatus.notInstalled, version }); + } notifications.toasts.addWarning({ title: toMountPoint( { - return packages[pkg].status; - }, - [packages] + [getPackageInstallStatus, notifications.toasts, setPackageInstallStatus, toDetailView] ); const uninstallPackage = useCallback( async ({ name, version, title }: Pick) => { - setPackageInstallStatus({ name, status: InstallStatus.uninstalling }); + setPackageInstallStatus({ name, status: InstallStatus.uninstalling, version }); const pkgkey = `${name}-${version}`; const res = await sendRemovePackage(pkgkey); if (res.error) { - setPackageInstallStatus({ name, status: InstallStatus.installed }); + setPackageInstallStatus({ name, status: InstallStatus.installed, version }); notifications.toasts.addWarning({ title: toMountPoint( ; export function ContentPanel(props: ContentPanelProps) { - const { panel, name, version, assets, title, removable } = props; + const { panel, name, version, assets, title, removable, latestVersion } = props; switch (panel) { case 'settings': return ( @@ -60,6 +60,7 @@ export function ContentPanel(props: ContentPanelProps) { assets={assets} title={title} removable={removable} + latestVersion={latestVersion} /> ); case 'data-sources': diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx index fa3245aec02c5b..c82b7ed2297a7c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx @@ -20,7 +20,7 @@ export const DataSourcesPanel = ({ name, version }: DataSourcesPanelProps) => { const packageInstallStatus = getPackageInstallStatus(name); // if they arrive at this page and the package is not installed, send them to overview // this happens if they arrive with a direct url or they uninstall while on this tab - if (packageInstallStatus !== InstallStatus.installed) + if (packageInstallStatus.status !== InstallStatus.installed) return ( props.theme.eui.euiSizeM}; `; -const StyledVersion = styled(Version)` - font-size: ${props => props.theme.eui.euiFontSizeS}; - color: ${props => props.theme.eui.euiColorDarkShade}; -`; - type HeaderProps = PackageInfo & { iconType?: IconType }; export function Header(props: HeaderProps) { - const { iconType, name, title, version } = props; + const { iconType, name, title, version, installedVersion, latestVersion } = props; const hasWriteCapabilites = useCapabilities().write; const { toListView } = useLinks(); const ADD_DATASOURCE_URI = useLink(`${EPM_PATH}/${name}-${version}/add-datasource`); - + const updateAvailable = installedVersion && installedVersion < latestVersion ? true : false; return ( @@ -59,7 +54,11 @@ export function Header(props: HeaderProps) {

{title} - + + + {version} {updateAvailable && } + +

diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx index 3239d7b90e3c3c..1f3eb2cc9362e5 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx @@ -32,11 +32,12 @@ export function Detail() { const packageInfo = response.data?.response; const title = packageInfo?.title; const name = packageInfo?.name; + const installedVersion = packageInfo?.installedVersion; const status: InstallStatus = packageInfo?.status as any; // track install status state if (name) { - setPackageInstallStatus({ name, status }); + setPackageInstallStatus({ name, status, version: installedVersion || null }); } if (packageInfo) { setInfo({ ...packageInfo, title: title || '' }); @@ -64,7 +65,6 @@ type LayoutProps = PackageInfo & Pick & Pick diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx index cbbf1ce53c4ea3..cdad67fd875483 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx @@ -13,19 +13,21 @@ import { ConfirmPackageUninstall } from './confirm_package_uninstall'; import { ConfirmPackageInstall } from './confirm_package_install'; type InstallationButtonProps = Pick & { - disabled: boolean; + disabled?: boolean; + isUpdate?: boolean; }; export function InstallationButton(props: InstallationButtonProps) { - const { assets, name, title, version, disabled = true } = props; + const { assets, name, title, version, disabled = true, isUpdate = false } = props; const hasWriteCapabilites = useCapabilities().write; const installPackage = useInstallPackage(); const uninstallPackage = useUninstallPackage(); const getPackageInstallStatus = useGetPackageInstallStatus(); - const installationStatus = getPackageInstallStatus(name); + const { status: installationStatus } = getPackageInstallStatus(name); const isInstalling = installationStatus === InstallStatus.installing; const isRemoving = installationStatus === InstallStatus.uninstalling; const isInstalled = installationStatus === InstallStatus.installed; + const showUninstallButton = isInstalled || isRemoving; const [isModalVisible, setModalVisible] = useState(false); const toggleModal = useCallback(() => { setModalVisible(!isModalVisible); @@ -36,6 +38,10 @@ export function InstallationButton(props: InstallationButtonProps) { toggleModal(); }, [installPackage, name, title, toggleModal, version]); + const handleClickUpdate = useCallback(() => { + installPackage({ name, version, title, fromUpdate: true }); + }, [installPackage, name, title, version]); + const handleClickUninstall = useCallback(() => { uninstallPackage({ name, version, title }); toggleModal(); @@ -78,6 +84,15 @@ export function InstallationButton(props: InstallationButtonProps) { ); + const updateButton = ( + + + + ); + const uninstallButton = ( - {isInstalled || isRemoving ? uninstallButton : installButton} + {isUpdate ? updateButton : showUninstallButton ? uninstallButton : installButton} {isModalVisible && (isInstalled ? uninstallModal : installModal)}
) : null; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx index 3589a1a9444e1b..4d4dba2a64e5a6 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx @@ -8,11 +8,22 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { EuiSpacer } from '@elastic/eui'; +import styled from 'styled-components'; import { InstallStatus, PackageInfo } from '../../../../types'; import { useGetDatasources } from '../../../../hooks'; import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../constants'; import { useGetPackageInstallStatus } from '../../hooks'; import { InstallationButton } from './installation_button'; +import { UpdateIcon } from '../../components/icons'; + +const SettingsTitleCell = styled.td` + padding-right: ${props => props.theme.eui.spacerSizes.xl}; + padding-bottom: ${props => props.theme.eui.spacerSizes.m}; +`; + +const UpdatesAvailableMsgContainer = styled.span` + padding-left: ${props => props.theme.eui.spacerSizes.s}; +`; const NoteLabel = () => ( ( defaultMessage="Note:" /> ); +const UpdatesAvailableMsg = () => ( + + + + +); + export const SettingsPanel = ( - props: Pick + props: Pick ) => { + const { name, title, removable, latestVersion, version } = props; const getPackageInstallStatus = useGetPackageInstallStatus(); const { data: datasourcesData } = useGetDatasources({ perPage: 0, page: 1, kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name:${props.name}`, }); - const { name, title, removable } = props; - const packageInstallStatus = getPackageInstallStatus(name); + const { status: installationStatus, version: installedVersion } = getPackageInstallStatus(name); const packageHasDatasources = !!datasourcesData?.total; + const updateAvailable = installedVersion && installedVersion < latestVersion ? true : false; + const isViewingOldPackage = version < latestVersion; + // hide install/remove options if the user has version of the package is installed + // and this package is out of date or if they do have a version installed but it's not this one + const hideInstallOptions = + (installationStatus === InstallStatus.notInstalled && isViewingOldPackage) || + (installationStatus === InstallStatus.installed && installedVersion !== version); + + const isUpdating = installationStatus === InstallStatus.installing && installedVersion; return ( @@ -43,14 +73,13 @@ export const SettingsPanel = ( - {packageInstallStatus === InstallStatus.notInstalled || - packageInstallStatus === InstallStatus.installing ? ( + {installedVersion !== null && (

-

- -

+ + + + + + + + + + + + + + + +
+ + {installedVersion} + + {updateAvailable && } +
+ + {latestVersion} + +
+ {updateAvailable && ( +

+ +

+ )}

- ) : ( + )} + {!hideInstallOptions && !isUpdating && (
- -

+ + {installationStatus === InstallStatus.notInstalled || + installationStatus === InstallStatus.installing ? ( +
+ +

+ +

+
+ +

+ +

+
+ ) : ( +
+ +

+ +

+
+ +

+ +

+
+ )} + + +

+ +

+
+
+ {packageHasDatasources && removable === true && ( +

+ + + ), }} /> -

-
- -

- -

+

+ )} + {removable === false && ( +

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

+ )}
)} - - -

- -

-
-
- {packageHasDatasources && removable === true && ( -

- - - - ), - }} - /> -

- )} - {removable === false && ( -

- - - - ), - }} - /> -

- )}
); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx index 05729ccfc1af42..ab168ef1530bd6 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx @@ -37,7 +37,7 @@ export function SideNavLinks({ name, version, active }: NavLinkProps) { : p.theme.eui.euiFontWeightRegular}; `; // don't display Data Sources tab if the package is not installed - if (packageInstallStatus !== InstallStatus.installed && panel === 'data-sources') + if (packageInstallStatus.status !== InstallStatus.installed && panel === 'data-sources') return null; return ( diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts index d76584225877c2..da8d79a04b97cd 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts @@ -67,9 +67,10 @@ export async function getPackageInfo(options: { pkgVersion: string; }): Promise { const { savedObjectsClient, pkgName, pkgVersion } = options; - const [item, savedObject, assets] = await Promise.all([ + const [item, savedObject, latestPackage, assets] = await Promise.all([ Registry.fetchInfo(pkgName, pkgVersion), getInstallationObject({ savedObjectsClient, pkgName }), + Registry.fetchFindLatestPackage(pkgName), Registry.getArchiveInfo(pkgName, pkgVersion), ] as const); // adding `as const` due to regression in TS 3.7.2 @@ -79,6 +80,7 @@ export async function getPackageInfo(options: { // add properties that aren't (or aren't yet) on Registry response const updated = { ...item, + latestVersion: latestPackage.version, title: item.title || nameAsTitle(item.name), assets: Registry.groupPathsByService(assets || []), }; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts index d49e0e661440f3..c67cccd044bf5e 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts @@ -43,6 +43,7 @@ export function createInstallableFrom( ? { ...from, status: InstallationStatus.installed, + installedVersion: savedObject.attributes.version, savedObject, } : { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 06f3decdbbe6f2..8f51c4d78305c0 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -106,7 +106,7 @@ export async function installPackage(options: { try { await deleteKibanaSavedObjectsAssets(savedObjectsClient, installedPkg.attributes.installed); } catch (err) { - // some assets may not exist if deleting during a failed update + // log these errors, some assets may not exist if deleted during a failed update } } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts index ac2c869f3b9e9f..befb4722b6504f 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts @@ -121,8 +121,12 @@ export async function deleteKibanaSavedObjectsAssets( const deletePromises = installedObjects.map(({ id, type }) => { const assetType = type as AssetType; if (savedObjectTypes.includes(assetType)) { - savedObjectsClient.delete(assetType, id); + return savedObjectsClient.delete(assetType, id); } }); - await Promise.all(deletePromises); + try { + await Promise.all(deletePromises); + } catch (err) { + throw new Error('error deleting saved object asset'); + } } From f5a8d8e6c27958dacc16a36b24bded57b4313f9d Mon Sep 17 00:00:00 2001 From: Lee Drengenberg Date: Wed, 29 Apr 2020 19:05:45 +0000 Subject: [PATCH 07/30] make inserting timestamp with navigate methods optional with default true (#64655) --- test/functional/page_objects/common_page.ts | 23 +++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 862e5127bb6704..93debdcc37f0ab 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -44,6 +44,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo ensureCurrentUrl: boolean; shouldLoginIfPrompted: boolean; useActualUrl: boolean; + insertTimestamp: boolean; } class CommonPage { @@ -65,7 +66,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo * Logins to Kibana as default user and navigates to provided app * @param appUrl Kibana URL */ - private async loginIfPrompted(appUrl: string) { + private async loginIfPrompted(appUrl: string, insertTimestamp: boolean) { let currentUrl = await browser.getCurrentUrl(); log.debug(`currentUrl = ${currentUrl}\n appUrl = ${appUrl}`); await testSubjects.find('kibanaChrome', 6 * defaultFindTimeout); // 60 sec waiting @@ -87,7 +88,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo '[data-test-subj="kibanaChrome"] nav:not(.ng-hide)', 6 * defaultFindTimeout ); - await browser.get(appUrl); + await browser.get(appUrl, insertTimestamp); currentUrl = await browser.getCurrentUrl(); log.debug(`Finished login process currentUrl = ${currentUrl}`); } @@ -95,7 +96,13 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo } private async navigate(navigateProps: NavigateProps) { - const { appConfig, ensureCurrentUrl, shouldLoginIfPrompted, useActualUrl } = navigateProps; + const { + appConfig, + ensureCurrentUrl, + shouldLoginIfPrompted, + useActualUrl, + insertTimestamp, + } = navigateProps; const appUrl = getUrl.noAuth(config.get('servers.kibana'), appConfig); await retry.try(async () => { @@ -111,7 +118,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo } const currentUrl = shouldLoginIfPrompted - ? await this.loginIfPrompted(appUrl) + ? await this.loginIfPrompted(appUrl, insertTimestamp) : await browser.getCurrentUrl(); if (ensureCurrentUrl && !currentUrl.includes(appUrl)) { @@ -134,6 +141,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo ensureCurrentUrl = true, shouldLoginIfPrompted = true, useActualUrl = false, + insertTimestamp = true, } = {} ) { const appConfig = { @@ -146,6 +154,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo ensureCurrentUrl, shouldLoginIfPrompted, useActualUrl, + insertTimestamp, }); } @@ -165,6 +174,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo ensureCurrentUrl = true, shouldLoginIfPrompted = true, useActualUrl = true, + insertTimestamp = true, } = {} ) { const appConfig = { @@ -178,6 +188,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo ensureCurrentUrl, shouldLoginIfPrompted, useActualUrl, + insertTimestamp, }); } @@ -208,7 +219,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo async navigateToApp( appName: string, - { basePath = '', shouldLoginIfPrompted = true, hash = '' } = {} + { basePath = '', shouldLoginIfPrompted = true, hash = '', insertTimestamp = true } = {} ) { let appUrl: string; if (config.has(['apps', appName])) { @@ -239,7 +250,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo log.debug('returned from get, calling refresh'); await browser.refresh(); let currentUrl = shouldLoginIfPrompted - ? await this.loginIfPrompted(appUrl) + ? await this.loginIfPrompted(appUrl, insertTimestamp) : await browser.getCurrentUrl(); if (currentUrl.includes('app/kibana')) { From 02ba5fcb131dc82c4d253a8e49e40d3d9e600037 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Wed, 29 Apr 2020 12:19:31 -0700 Subject: [PATCH 08/30] [Reporting/Test] Convert functional test code to Typescript (#64601) * Convert the tests to typescript * remove outdated comment * fix typescript * fix "as any" Co-authored-by: Elastic Machine --- .../reporting/{index.js => index.ts} | 9 ++--- .../lib/{compare_pngs.js => compare_pngs.ts} | 13 ++++--- .../discover/{reporting.js => reporting.ts} | 3 +- .../visualize/{reporting.js => reporting.ts} | 3 +- x-pack/test/functional/page_objects/index.ts | 1 - .../{reporting_page.js => reporting_page.ts} | 35 ++++++++----------- 6 files changed, 33 insertions(+), 31 deletions(-) rename x-pack/test/functional/apps/dashboard/reporting/{index.js => index.ts} (94%) rename x-pack/test/functional/apps/dashboard/reporting/lib/{compare_pngs.js => compare_pngs.ts} (90%) rename x-pack/test/functional/apps/discover/{reporting.js => reporting.ts} (96%) rename x-pack/test/functional/apps/visualize/{reporting.js => reporting.ts} (94%) rename x-pack/test/functional/page_objects/{reporting_page.js => reporting_page.ts} (84%) diff --git a/x-pack/test/functional/apps/dashboard/reporting/index.js b/x-pack/test/functional/apps/dashboard/reporting/index.ts similarity index 94% rename from x-pack/test/functional/apps/dashboard/reporting/index.js rename to x-pack/test/functional/apps/dashboard/reporting/index.ts index 99be084d80d74c..796e15b4e270f4 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/index.js +++ b/x-pack/test/functional/apps/dashboard/reporting/index.ts @@ -5,9 +5,10 @@ */ import expect from '@kbn/expect'; -import path from 'path'; import fs from 'fs'; +import path from 'path'; import { promisify } from 'util'; +import { FtrProviderContext } from '../../../ftr_provider_context'; import { checkIfPngsMatch } from './lib/compare_pngs'; const writeFileAsync = promisify(fs.writeFile); @@ -15,7 +16,7 @@ const mkdirAsync = promisify(fs.mkdir); const REPORTS_FOLDER = path.resolve(__dirname, 'reports'); -export default function({ getService, getPageObjects }) { +export default function({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); const log = getService('log'); @@ -85,14 +86,14 @@ export default function({ getService, getPageObjects }) { describe('Preserve Layout', () => { it('matches baseline report', async function() { - const writeSessionReport = async (name, rawPdf, reportExt) => { + const writeSessionReport = async (name: string, rawPdf: Buffer, reportExt: string) => { const sessionDirectory = path.resolve(REPORTS_FOLDER, 'session'); await mkdirAsync(sessionDirectory, { recursive: true }); const sessionReportPath = path.resolve(sessionDirectory, `${name}.${reportExt}`); await writeFileAsync(sessionReportPath, rawPdf); return sessionReportPath; }; - const getBaselineReportPath = (fileName, reportExt) => { + const getBaselineReportPath = (fileName: string, reportExt: string) => { const baselineFolder = path.resolve(REPORTS_FOLDER, 'baseline'); const fullPath = path.resolve(baselineFolder, `${fileName}.${reportExt}`); log.debug(`getBaselineReportPath (${fullPath})`); diff --git a/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.js b/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts similarity index 90% rename from x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.js rename to x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts index 13c97a7fce7858..b2eb645c8372c0 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.js +++ b/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts @@ -4,14 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import path from 'path'; -import fs from 'fs'; import { promisify } from 'bluebird'; +import fs from 'fs'; +import path from 'path'; import { comparePngs } from '../../../../../../../test/functional/services/lib/compare_pngs'; -const mkdirAsync = promisify(fs.mkdir); +const mkdirAsync = promisify(fs.mkdir); -export async function checkIfPngsMatch(actualpngPath, baselinepngPath, screenshotsDirectory, log) { +export async function checkIfPngsMatch( + actualpngPath: string, + baselinepngPath: string, + screenshotsDirectory: string, + log: any +) { log.debug(`checkIfpngsMatch: ${actualpngPath} vs ${baselinepngPath}`); // Copy the pngs into the screenshot session directory, as that's where the generated pngs will automatically be // stored. diff --git a/x-pack/test/functional/apps/discover/reporting.js b/x-pack/test/functional/apps/discover/reporting.ts similarity index 96% rename from x-pack/test/functional/apps/discover/reporting.js rename to x-pack/test/functional/apps/discover/reporting.ts index 4aa005fc2db553..7a33e7f5135d48 100644 --- a/x-pack/test/functional/apps/discover/reporting.js +++ b/x-pack/test/functional/apps/discover/reporting.ts @@ -5,8 +5,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function({ getService, getPageObjects }) { +export default function({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const esArchiver = getService('esArchiver'); const browser = getService('browser'); diff --git a/x-pack/test/functional/apps/visualize/reporting.js b/x-pack/test/functional/apps/visualize/reporting.ts similarity index 94% rename from x-pack/test/functional/apps/visualize/reporting.js rename to x-pack/test/functional/apps/visualize/reporting.ts index bc252e1ad4134b..5ef954e334d815 100644 --- a/x-pack/test/functional/apps/visualize/reporting.js +++ b/x-pack/test/functional/apps/visualize/reporting.ts @@ -5,8 +5,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function({ getService, getPageObjects }) { +export default function({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); const log = getService('log'); diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index 782d57adea7707..4b8c2944ef190f 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -19,7 +19,6 @@ import { GraphPageProvider } from './graph_page'; import { GrokDebuggerPageProvider } from './grok_debugger_page'; // @ts-ignore not ts yet import { WatcherPageProvider } from './watcher_page'; -// @ts-ignore not ts yet import { ReportingPageProvider } from './reporting_page'; // @ts-ignore not ts yet import { AccountSettingProvider } from './accountsetting_page'; diff --git a/x-pack/test/functional/page_objects/reporting_page.js b/x-pack/test/functional/page_objects/reporting_page.ts similarity index 84% rename from x-pack/test/functional/page_objects/reporting_page.js rename to x-pack/test/functional/page_objects/reporting_page.ts index b24ba8cf95d1ce..2c20519a8d2140 100644 --- a/x-pack/test/functional/page_objects/reporting_page.js +++ b/x-pack/test/functional/page_objects/reporting_page.ts @@ -4,25 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ +import http, { IncomingMessage } from 'http'; +import { FtrProviderContext } from 'test/functional/ftr_provider_context'; import { parse } from 'url'; -import http from 'http'; -/* - * NOTE: Reporting is a service, not an app. The page objects that are - * important for generating reports belong to the apps that integrate with the - * Reporting service. Eventually, this file should be dissolved across the - * apps that need it for testing their integration. - * Issue: https://github.com/elastic/kibana/issues/52927 - */ -export function ReportingPageProvider({ getService, getPageObjects }) { +export function ReportingPageProvider({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const log = getService('log'); const testSubjects = getService('testSubjects'); const browser = getService('browser'); - const PageObjects = getPageObjects(['common', 'security', 'share', 'timePicker']); + const PageObjects = getPageObjects(['common', 'security' as any, 'share', 'timePicker']); // FIXME: Security PageObject is not Typescript class ReportingPage { - async forceSharedItemsContainerSize({ width }) { + async forceSharedItemsContainerSize({ width }: { width: number }) { await browser.execute(` var el = document.querySelector('[data-shared-items-container]'); el.style.flex="none"; @@ -30,7 +24,7 @@ export function ReportingPageProvider({ getService, getPageObjects }) { `); } - async getReportURL(timeout) { + async getReportURL(timeout: number) { log.debug('getReportURL'); const url = await testSubjects.getAttribute('downloadCompletedReportButton', 'href', timeout); @@ -48,7 +42,7 @@ export function ReportingPageProvider({ getService, getPageObjects }) { `); } - getResponse(url) { + getResponse(url: string): Promise { log.debug(`getResponse for ${url}`); const auth = 'test_user:changeme'; // FIXME not sure why there is no config that can be read for this const headers = { @@ -62,29 +56,30 @@ export function ReportingPageProvider({ getService, getPageObjects }) { hostname: parsedUrl.hostname, path: parsedUrl.path, port: parsedUrl.port, - responseType: 'arraybuffer', headers, }, - res => { + (res: IncomingMessage) => { resolve(res); } ) - .on('error', e => { + .on('error', (e: Error) => { log.error(e); reject(e); }); }); } - async getRawPdfReportData(url) { - const data = []; // List of Buffer objects + async getRawPdfReportData(url: string): Promise { + const data: Buffer[] = []; // List of Buffer objects log.debug(`getRawPdfReportData for ${url}`); return new Promise(async (resolve, reject) => { const response = await this.getResponse(url).catch(reject); - response.on('data', chunk => data.push(chunk)); - response.on('end', () => resolve(Buffer.concat(data))); + if (response) { + response.on('data', (chunk: Buffer) => data.push(chunk)); + response.on('end', () => resolve(Buffer.concat(data))); + } }); } From d6f0c785d916660b9347f25bb6e3034989f8432a Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 29 Apr 2020 13:27:55 -0600 Subject: [PATCH 09/30] [Maps] do not display EMS or kibana layer wizards when not configured (#64554) * [Maps] do not display EMS or kibana layer wizards when not configured * default tilemap to empty object instead of array * fix typo Co-authored-by: Elastic Machine --- x-pack/legacy/plugins/maps/index.js | 2 +- .../plugins/maps/public/layers/layer_wizard_registry.ts | 5 ++++- .../ems_file_source/ems_boundaries_layer_wizard.tsx | 5 +++++ .../sources/ems_tms_source/ems_base_map_layer_wizard.tsx | 5 +++++ .../kibana_regionmap_layer_wizard.tsx | 6 ++++++ .../kibana_base_map_layer_wizard.tsx | 6 ++++++ x-pack/plugins/maps/public/meta.js | 9 ++++++--- 7 files changed, 33 insertions(+), 5 deletions(-) diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js index d1e8892fa2c984..a1186e04ee27a6 100644 --- a/x-pack/legacy/plugins/maps/index.js +++ b/x-pack/legacy/plugins/maps/index.js @@ -54,7 +54,7 @@ export function maps(kibana) { emsLandingPageUrl: mapConfig.emsLandingPageUrl, kbnPkgVersion: serverConfig.get('pkg.version'), regionmapLayers: _.get(mapConfig, 'regionmap.layers', []), - tilemap: _.get(mapConfig, 'tilemap', []), + tilemap: _.get(mapConfig, 'tilemap', {}), }; }, styleSheetPaths: `${__dirname}/public/index.scss`, diff --git a/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts b/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts index 633e8c86d8c94f..7715541b1c52d7 100644 --- a/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts +++ b/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts @@ -20,6 +20,7 @@ export type RenderWizardArguments = { }; export type LayerWizard = { + checkVisibility?: () => boolean; description: string; icon: string; isIndexingSource?: boolean; @@ -34,5 +35,7 @@ export function registerLayerWizard(layerWizard: LayerWizard) { } export function getLayerWizards(): LayerWizard[] { - return [...registry]; + return registry.filter(layerWizard => { + return layerWizard.checkVisibility ? layerWizard.checkVisibility() : true; + }); } diff --git a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx index f31e770df2d958..a6e2e7f42657c3 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx @@ -12,8 +12,13 @@ import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry' import { EMSFileCreateSourceEditor } from './create_source_editor'; // @ts-ignore import { EMSFileSource, sourceTitle } from './ems_file_source'; +// @ts-ignore +import { isEmsEnabled } from '../../../meta'; export const emsBoundariesLayerWizardConfig: LayerWizard = { + checkVisibility: () => { + return isEmsEnabled(); + }, description: i18n.translate('xpack.maps.source.emsFileDescription', { defaultMessage: 'Administrative boundaries from Elastic Maps Service', }), diff --git a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx index ced33a0bcf84a9..fc745edbabee87 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx @@ -12,8 +12,13 @@ import { EMSTMSSource, sourceTitle } from './ems_tms_source'; import { VectorTileLayer } from '../../vector_tile_layer'; // @ts-ignore import { TileServiceSelect } from './tile_service_select'; +// @ts-ignore +import { isEmsEnabled } from '../../../meta'; export const emsBaseMapLayerWizardConfig: LayerWizard = { + checkVisibility: () => { + return isEmsEnabled(); + }, description: i18n.translate('xpack.maps.source.emsTileDescription', { defaultMessage: 'Tile map service from Elastic Maps Service', }), diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx index 4321501760fafd..a9adec2bda2c81 100644 --- a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx @@ -12,8 +12,14 @@ import { KibanaRegionmapSource, sourceTitle } from './kibana_regionmap_source'; import { VectorLayer } from '../../vector_layer'; // @ts-ignore import { CreateSourceEditor } from './create_source_editor'; +// @ts-ignore +import { getKibanaRegionList } from '../../../meta'; export const kibanaRegionMapLayerWizardConfig: LayerWizard = { + checkVisibility: () => { + const regions = getKibanaRegionList(); + return regions.length; + }, description: i18n.translate('xpack.maps.source.kbnRegionMapDescription', { defaultMessage: 'Vector data from hosted GeoJSON configured in kibana.yml', }), diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx index aeea2d6084f847..141fabeedd3e51 100644 --- a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx @@ -12,8 +12,14 @@ import { CreateSourceEditor } from './create_source_editor'; // @ts-ignore import { KibanaTilemapSource, sourceTitle } from './kibana_tilemap_source'; import { TileLayer } from '../../tile_layer'; +// @ts-ignore +import { getKibanaTileMap } from '../../../meta'; export const kibanaBasemapLayerWizardConfig: LayerWizard = { + checkVisibility: () => { + const tilemap = getKibanaTileMap(); + return !!tilemap.url; + }, description: i18n.translate('xpack.maps.source.kbnTMSDescription', { defaultMessage: 'Tile map service configured in kibana.yml', }), diff --git a/x-pack/plugins/maps/public/meta.js b/x-pack/plugins/maps/public/meta.js index d4612554cf00b9..c3245e8e98db2e 100644 --- a/x-pack/plugins/maps/public/meta.js +++ b/x-pack/plugins/maps/public/meta.js @@ -36,12 +36,15 @@ function fetchFunction(...args) { return fetch(...args); } +export function isEmsEnabled() { + return getInjectedVarFunc()('isEmsEnabled', true); +} + let emsClient = null; let latestLicenseId = null; export function getEMSClient() { if (!emsClient) { - const isEmsEnabled = getInjectedVarFunc()('isEmsEnabled', true); - if (isEmsEnabled) { + if (isEmsEnabled()) { const proxyElasticMapsServiceInMaps = getInjectedVarFunc()( 'proxyElasticMapsServiceInMaps', false @@ -86,7 +89,7 @@ export function getEMSClient() { } export function getGlyphUrl() { - if (!getInjectedVarFunc()('isEmsEnabled', true)) { + if (!isEmsEnabled()) { return ''; } return getInjectedVarFunc()('proxyElasticMapsServiceInMaps', false) From 2e410d8952324efdc43dd60e5470d4db3bd5257f Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 29 Apr 2020 12:53:06 -0700 Subject: [PATCH 10/30] skip flaky suite (#64812) (#64723) --- .../test_suites/event_log/public_api_integration.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts index e5b840b335846f..d7bbc29bd861e2 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts @@ -19,7 +19,9 @@ export default function({ getService }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); - describe('Event Log public API', () => { + // FLAKY: https://github.com/elastic/kibana/issues/64723 + // FLAKY: https://github.com/elastic/kibana/issues/64812 + describe.skip('Event Log public API', () => { it('should allow querying for events by Saved Object', async () => { const id = uuid.v4(); From 1669a1044108a11b44b4ee998d55beb84e4e942f Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Wed, 29 Apr 2020 15:36:37 -0500 Subject: [PATCH 11/30] [Metrics UI] Fix alerting when a filter query is present (#64575) --- .../metric_threshold_executor.ts | 23 ++++++++++++------- .../apis/infra/metrics_alerting.ts | 18 +++++++++++++++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 946f1c14bf593a..cf691f73bdc2cc 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -51,17 +51,19 @@ const getCurrentValueFromAggregations = ( const getParsedFilterQuery: ( filterQuery: string | undefined -) => Record = filterQuery => { +) => Record | Array> = filterQuery => { if (!filterQuery) return {}; try { return JSON.parse(filterQuery).bool; } catch (e) { - return { - query_string: { - query: filterQuery, - analyze_wildcard: true, + return [ + { + query_string: { + query: filterQuery, + analyze_wildcard: true, + }, }, - }; + ]; } }; @@ -159,8 +161,12 @@ export const getElasticsearchMetricQuery = ( return { query: { bool: { - filter: [...rangeFilters, ...metricFieldFilters], - ...parsedFilterQuery, + filter: [ + ...rangeFilters, + ...metricFieldFilters, + ...(Array.isArray(parsedFilterQuery) ? parsedFilterQuery : []), + ], + ...(!Array.isArray(parsedFilterQuery) ? parsedFilterQuery : {}), }, }, size: 0, @@ -233,6 +239,7 @@ const getMetric: ( body: searchBody, index, }); + return { '*': getCurrentValueFromAggregations(result.aggregations, aggType) }; } catch (e) { return { '*': undefined }; // Trigger an Error state diff --git a/x-pack/test/api_integration/apis/infra/metrics_alerting.ts b/x-pack/test/api_integration/apis/infra/metrics_alerting.ts index 4f17f9db674839..19879f5761ab2c 100644 --- a/x-pack/test/api_integration/apis/infra/metrics_alerting.ts +++ b/x-pack/test/api_integration/apis/infra/metrics_alerting.ts @@ -39,6 +39,7 @@ export default function({ getService }: FtrProviderContext) { }); expect(result.error).to.not.be.ok(); expect(result.hits).to.be.ok(); + expect(result.aggregations).to.be.ok(); }); } it('should work with a filterQuery', async () => { @@ -53,6 +54,21 @@ export default function({ getService }: FtrProviderContext) { }); expect(result.error).to.not.be.ok(); expect(result.hits).to.be.ok(); + expect(result.aggregations).to.be.ok(); + }); + it('should work with a filterQuery in KQL format', async () => { + const searchBody = getElasticsearchMetricQuery( + getSearchParams('avg'), + undefined, + '"agent.hostname":"foo"' + ); + const result = await client.search({ + index, + body: searchBody, + }); + expect(result.error).to.not.be.ok(); + expect(result.hits).to.be.ok(); + expect(result.aggregations).to.be.ok(); }); }); describe('querying with a groupBy parameter', () => { @@ -65,6 +81,7 @@ export default function({ getService }: FtrProviderContext) { }); expect(result.error).to.not.be.ok(); expect(result.hits).to.be.ok(); + expect(result.aggregations).to.be.ok(); }); } it('should work with a filterQuery', async () => { @@ -79,6 +96,7 @@ export default function({ getService }: FtrProviderContext) { }); expect(result.error).to.not.be.ok(); expect(result.hits).to.be.ok(); + expect(result.aggregations).to.be.ok(); }); }); }); From 6338cef317f023e3cfaebcc27a0c0fb1e9b857e8 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 29 Apr 2020 16:41:59 -0400 Subject: [PATCH 12/30] [Ingest] Allow to enable monitoring of elastic agent (#63598) --- .../common/constants/agent_config.ts | 1 + .../common/types/models/agent_config.ts | 9 ++ .../agent_config/components/config_form.tsx | 65 ++++++++- .../list_page/components/create_config.tsx | 1 + .../ingest_manager/server/saved_objects.ts | 1 + .../server/services/agent_config.test.ts | 134 ++++++++++++++++++ .../server/services/agent_config.ts | 45 ++++-- .../server/types/models/agent_config.ts | 3 + 8 files changed, 246 insertions(+), 13 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/server/services/agent_config.test.ts diff --git a/x-pack/plugins/ingest_manager/common/constants/agent_config.ts b/x-pack/plugins/ingest_manager/common/constants/agent_config.ts index c5067480fb9538..9bc1293799d3c5 100644 --- a/x-pack/plugins/ingest_manager/common/constants/agent_config.ts +++ b/x-pack/plugins/ingest_manager/common/constants/agent_config.ts @@ -14,6 +14,7 @@ export const DEFAULT_AGENT_CONFIG = { status: AgentConfigStatus.Active, datasources: [], is_default: true, + monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>, }; export const DEFAULT_AGENT_CONFIGS_PACKAGES = [DefaultPackages.system]; diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts index 2372caee512af6..7705956590c161 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts @@ -23,6 +23,7 @@ export interface NewAgentConfig { namespace?: string; description?: string; is_default?: boolean; + monitoring_enabled?: Array<'logs' | 'metrics'>; } export interface AgentConfig extends NewAgentConfig, SavedObjectAttributes { @@ -60,4 +61,12 @@ export interface FullAgentConfig { }; datasources: FullAgentConfigDatasource[]; revision?: number; + settings?: { + monitoring: { + use_output?: string; + enabled: boolean; + metrics: boolean; + logs: boolean; + }; + }; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx index 0d53ca34a1fefd..92c44d86e47c65 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx @@ -18,6 +18,7 @@ import { EuiText, EuiComboBox, EuiIconTip, + EuiCheckboxGroup, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -30,7 +31,7 @@ interface ValidationResults { const StyledEuiAccordion = styled(EuiAccordion)` .ingest-active-button { - color: ${props => props.theme.eui.euiColorPrimary}}; + color: ${props => props.theme.eui.euiColorPrimary}; } `; @@ -244,6 +245,68 @@ export const AgentConfigForm: React.FunctionComponent = ({ )} + + + + +

+ +

+
+ + + + +
+ + { + acc[key] = true; + return acc; + }, + { logs: false, metrics: false } + )} + onChange={id => { + if (id !== 'logs' && id !== 'metrics') { + return; + } + + const hasLogs = + agentConfig.monitoring_enabled && agentConfig.monitoring_enabled.indexOf(id) >= 0; + + const previousValues = agentConfig.monitoring_enabled || []; + updateAgentConfig({ + monitoring_enabled: hasLogs + ? previousValues.filter(type => type !== id) + : [...previousValues, id], + }); + }} + /> + +
); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx index 1fe116ef360901..9f582e7e2fbe69 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx @@ -34,6 +34,7 @@ export const CreateAgentConfigFlyout: React.FunctionComponent = ({ onClos description: '', namespace: '', is_default: undefined, + monitoring_enabled: ['logs', 'metrics'], }); const [isLoading, setIsLoading] = useState(false); const [withSysMonitoring, setWithSysMonitoring] = useState(true); diff --git a/x-pack/plugins/ingest_manager/server/saved_objects.ts b/x-pack/plugins/ingest_manager/server/saved_objects.ts index 882258e859555b..90fe68e61bb1b4 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects.ts @@ -128,6 +128,7 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { updated_on: { type: 'keyword' }, updated_by: { type: 'keyword' }, revision: { type: 'integer' }, + monitoring_enabled: { type: 'keyword' }, }, }, }, diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.test.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.test.ts new file mode 100644 index 00000000000000..17758f6e3d7f12 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.test.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { agentConfigService } from './agent_config'; +import { Output } from '../types'; + +function getSavedObjectMock(configAttributes: any) { + const mock = savedObjectsClientMock.create(); + + mock.get.mockImplementation(async (type: string, id: string) => { + return { + type, + id, + references: [], + attributes: configAttributes, + }; + }); + + return mock; +} + +jest.mock('./output', () => { + return { + outputService: { + getDefaultOutputId: () => 'test-id', + get: (): Output => { + return { + id: 'test-id', + is_default: true, + name: 'default', + // @ts-ignore + type: 'elasticsearch', + hosts: ['http://127.0.0.1:9201'], + }; + }, + }, + }; +}); + +describe('agent config', () => { + describe('getFullConfig', () => { + it('should return a config without monitoring if not monitoring is not enabled', async () => { + const soClient = getSavedObjectMock({ + revision: 1, + }); + const config = await agentConfigService.getFullConfig(soClient, 'config'); + + expect(config).toMatchObject({ + id: 'config', + outputs: { + default: { + type: 'elasticsearch', + hosts: ['http://127.0.0.1:9201'], + ca_sha256: undefined, + api_key: undefined, + }, + }, + datasources: [], + revision: 1, + settings: { + monitoring: { + enabled: false, + logs: false, + metrics: false, + }, + }, + }); + }); + + it('should return a config with monitoring if monitoring is enabled for logs', async () => { + const soClient = getSavedObjectMock({ + revision: 1, + monitoring_enabled: ['logs'], + }); + const config = await agentConfigService.getFullConfig(soClient, 'config'); + + expect(config).toMatchObject({ + id: 'config', + outputs: { + default: { + type: 'elasticsearch', + hosts: ['http://127.0.0.1:9201'], + ca_sha256: undefined, + api_key: undefined, + }, + }, + datasources: [], + revision: 1, + settings: { + monitoring: { + use_output: 'default', + enabled: true, + logs: true, + metrics: false, + }, + }, + }); + }); + + it('should return a config with monitoring if monitoring is enabled for metrics', async () => { + const soClient = getSavedObjectMock({ + revision: 1, + monitoring_enabled: ['metrics'], + }); + const config = await agentConfigService.getFullConfig(soClient, 'config'); + + expect(config).toMatchObject({ + id: 'config', + outputs: { + default: { + type: 'elasticsearch', + hosts: ['http://127.0.0.1:9201'], + ca_sha256: undefined, + api_key: undefined, + }, + }, + datasources: [], + revision: 1, + settings: { + monitoring: { + use_output: 'default', + enabled: true, + logs: false, + metrics: true, + }, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.ts index 75bbfc21293c2b..7ab6ef1920c18a 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts @@ -301,28 +301,49 @@ class AgentConfigService { if (!config) { return null; } + const defaultOutput = await outputService.get( + soClient, + await outputService.getDefaultOutputId(soClient) + ); const agentConfig: FullAgentConfig = { id: config.id, outputs: { // TEMPORARY as we only support a default output - ...[ - await outputService.get(soClient, await outputService.getDefaultOutputId(soClient)), - ].reduce((outputs, { config: outputConfig, name, type, hosts, ca_sha256, api_key }) => { - outputs[name] = { - type, - hosts, - ca_sha256, - api_key, - ...outputConfig, - }; - return outputs; - }, {} as FullAgentConfig['outputs']), + ...[defaultOutput].reduce( + (outputs, { config: outputConfig, name, type, hosts, ca_sha256, api_key }) => { + outputs[name] = { + type, + hosts, + ca_sha256, + api_key, + ...outputConfig, + }; + return outputs; + }, + {} as FullAgentConfig['outputs'] + ), }, datasources: (config.datasources as Datasource[]) .filter(datasource => datasource.enabled) .map(ds => storedDatasourceToAgentDatasource(ds)), revision: config.revision, + ...(config.monitoring_enabled && config.monitoring_enabled.length > 0 + ? { + settings: { + monitoring: { + use_output: defaultOutput.name, + enabled: true, + logs: config.monitoring_enabled.indexOf('logs') >= 0, + metrics: config.monitoring_enabled.indexOf('metrics') >= 0, + }, + }, + } + : { + settings: { + monitoring: { enabled: false, logs: false, metrics: false }, + }, + }), }; return agentConfig; diff --git a/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts b/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts index 040b2eb16289ab..59cadf3bd7f74c 100644 --- a/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts @@ -11,6 +11,9 @@ const AgentConfigBaseSchema = { name: schema.string(), namespace: schema.maybe(schema.string()), description: schema.maybe(schema.string()), + monitoring_enabled: schema.maybe( + schema.arrayOf(schema.oneOf([schema.literal('logs'), schema.literal('metrics')])) + ), }; export const NewAgentConfigSchema = schema.object({ From 110648258c6635e3bc936c389e06533b00960981 Mon Sep 17 00:00:00 2001 From: Henry Harding Date: Wed, 29 Apr 2020 17:18:51 -0400 Subject: [PATCH 13/30] Improve alpha messaging (#64692) * use fixed table layout * add alpha messaging flyout * Added alpha badge + data streams link * Update x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx Co-Authored-By: Jen Huang * Update x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx Co-Authored-By: Jen Huang * Update x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx Co-Authored-By: Jen Huang * Update x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx Co-Authored-By: Jen Huang * remove small tags * change messaging from alpha to experimental * add period * remove unused imports * fixed i18n ids Co-authored-by: Jen Huang Co-authored-by: Elastic Machine --- .../components/alpha_flyout.tsx | 101 ++++++++++++++++++ .../components/alpha_messaging.tsx | 50 +++++---- .../sections/overview/index.tsx | 24 ++++- 3 files changed, 153 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx new file mode 100644 index 00000000000000..1e7a14e3502296 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { + EuiButtonEmpty, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiFlyoutFooter, + EuiLink, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +interface Props { + onClose: () => void; +} + +export const AlphaFlyout: React.FunctionComponent = ({ onClose }) => { + return ( + + + +

+ +

+
+
+ + +

+ +

+ + + + ), + forumLink: ( + + + + ), + }} + /> +

+ +

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

+
+
+ + + + + +
+ ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx index 0f3ddee29fa443..5a06a9a8794412 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx @@ -3,35 +3,45 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useState } from 'react'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiText } from '@elastic/eui'; +import { EuiText, EuiLink } from '@elastic/eui'; +import { AlphaFlyout } from './alpha_flyout'; const Message = styled(EuiText).attrs(props => ({ color: 'subdued', textAlign: 'center', + size: 's', }))` padding: ${props => props.theme.eui.paddingSizes.m}; `; -export const AlphaMessaging: React.FC<{}> = () => ( - -

- - +export const AlphaMessaging: React.FC<{}> = () => { + const [isAlphaFlyoutOpen, setIsAlphaFlyoutOpen] = useState(false); + + return ( + <> + +

+ + + + {' – '} - - {' – '} - - -

-
-); + />{' '} + setIsAlphaFlyoutOpen(true)}> + View more details. + +

+ + {isAlphaFlyoutOpen && setIsAlphaFlyoutOpen(false)} />} + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx index 05d150fd9ae231..70d8e7d6882f88 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx @@ -8,6 +8,7 @@ import styled from 'styled-components'; import { EuiButton, EuiButtonEmpty, + EuiBetaBadge, EuiPanel, EuiText, EuiTitle, @@ -19,10 +20,11 @@ import { EuiFlexItem, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { WithHeaderLayout } from '../../layouts'; import { useLink, useGetAgentConfigs } from '../../hooks'; import { AgentEnrollmentFlyout } from '../fleet/agent_list_page/components'; -import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH } from '../../constants'; +import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from '../../constants'; const OverviewPanel = styled(EuiPanel).attrs(props => ({ paddingSize: 'm', @@ -57,6 +59,11 @@ const OverviewStats = styled(EuiDescriptionList).attrs(props => ({ } `; +const AlphaBadge = styled(EuiBetaBadge)` + vertical-align: top; + margin-left: ${props => props.theme.eui.paddingSizes.s}; +`; + export const IngestManagerOverview: React.FunctionComponent = () => { // Agent enrollment flyout state const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false); @@ -79,6 +86,19 @@ export const IngestManagerOverview: React.FunctionComponent = () => { id="xpack.ingestManager.overviewPageTitle" defaultMessage="Ingest Manager" /> + @@ -213,7 +233,7 @@ export const IngestManagerOverview: React.FunctionComponent = () => { /> - + Date: Wed, 29 Apr 2020 19:14:47 -0400 Subject: [PATCH 14/30] [Lens] Use suggestion system in chart switcher for subtypes (#64613) Co-authored-by: Elastic Machine --- .../datatable_visualization/visualization.tsx | 4 ++ .../config_panel/chart_switch.test.tsx | 33 +++++++++--- .../config_panel/chart_switch.tsx | 25 ++++++--- .../editor_frame/suggestion_helpers.ts | 9 +++- .../public/editor_frame_service/mocks.tsx | 1 + .../metric_visualization.tsx | 4 ++ x-pack/plugins/lens/public/types.ts | 9 ++++ .../xy_visualization/xy_visualization.test.ts | 41 +++++++++++++- .../xy_visualization/xy_visualization.tsx | 53 ++++++++++++------- 9 files changed, 143 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index 359c06a6a9ebce..48729448b2ea57 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -41,6 +41,10 @@ export const datatableVisualization: Visualization< }, ], + getVisualizationTypeId() { + return 'lnsDatatable'; + }, + getLayerIds(state) { return state.layers.map(l => l.layerId); }, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx index 3c61d270b1bcf9..c8d8064e60e38a 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx @@ -62,7 +62,25 @@ describe('chart_switch', () => { id: 'subvisC2', label: 'C2', }, + { + icon: 'empty', + id: 'subvisC3', + label: 'C3', + }, ], + getSuggestions: jest.fn(options => { + if (options.subVisualizationId === 'subvisC2') { + return []; + } + return [ + { + score: 1, + title: '', + state: `suggestion`, + previewIcon: 'empty', + }, + ]; + }), }, }; } @@ -313,10 +331,11 @@ describe('chart_switch', () => { expect(getMenuItem('subvisB', component).prop('betaBadgeIconType')).toBeUndefined(); }); - it('should not indicate data loss if visualization is not changed', () => { + it('should not show a warning when the subvisualization is the same', () => { const dispatch = jest.fn(); const frame = mockFrame(['a', 'b', 'c']); const visualizations = mockVisualizations(); + visualizations.visC.getVisualizationTypeId.mockReturnValue('subvisC2'); const switchVisualizationType = jest.fn(() => 'therebedragons'); visualizations.visC.switchVisualizationType = switchVisualizationType; @@ -333,10 +352,10 @@ describe('chart_switch', () => { /> ); - expect(getMenuItem('subvisC2', component).prop('betaBadgeIconType')).toBeUndefined(); + expect(getMenuItem('subvisC2', component).prop('betaBadgeIconType')).not.toBeDefined(); }); - it('should remove all layers if there is no suggestion', () => { + it('should get suggestions when switching subvisualization', () => { const dispatch = jest.fn(); const visualizations = mockVisualizations(); visualizations.visB.getSuggestions.mockReturnValueOnce([]); @@ -377,7 +396,7 @@ describe('chart_switch', () => { const dispatch = jest.fn(); const frame = mockFrame(['a', 'b', 'c']); const visualizations = mockVisualizations(); - const switchVisualizationType = jest.fn(() => 'therebedragons'); + const switchVisualizationType = jest.fn(() => 'switched'); visualizations.visC.switchVisualizationType = switchVisualizationType; @@ -393,12 +412,12 @@ describe('chart_switch', () => { /> ); - switchTo('subvisC2', component); - expect(switchVisualizationType).toHaveBeenCalledWith('subvisC2', 'therebegriffins'); + switchTo('subvisC3', component); + expect(switchVisualizationType).toHaveBeenCalledWith('subvisC3', 'suggestion'); expect(dispatch).toHaveBeenCalledWith( expect.objectContaining({ type: 'SWITCH_VISUALIZATION', - initialState: 'therebedragons', + initialState: 'switched', }) ); expect(frame.removeLayers).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx index 1461449f3c1c8a..d73f83e75c0e4e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx @@ -105,7 +105,16 @@ export function ChartSwitch(props: Props) { const switchVisType = props.visualizationMap[visualizationId].switchVisualizationType || ((_type: string, initialState: unknown) => initialState); - if (props.visualizationId === visualizationId) { + const layers = Object.entries(props.framePublicAPI.datasourceLayers); + const containsData = layers.some( + ([_layerId, datasource]) => datasource.getTableSpec().length > 0 + ); + // Always show the active visualization as a valid selection + if ( + props.visualizationId === visualizationId && + props.visualizationState && + newVisualization.getVisualizationTypeId(props.visualizationState) === subVisualizationId + ) { return { visualizationId, subVisualizationId, @@ -116,13 +125,13 @@ export function ChartSwitch(props: Props) { }; } - const layers = Object.entries(props.framePublicAPI.datasourceLayers); - const containsData = layers.some( - ([_layerId, datasource]) => datasource.getTableSpec().length > 0 + const topSuggestion = getTopSuggestion( + props, + visualizationId, + newVisualization, + subVisualizationId ); - const topSuggestion = getTopSuggestion(props, visualizationId, newVisualization); - let dataLoss: VisualizationSelection['dataLoss']; if (!containsData) { @@ -250,7 +259,8 @@ export function ChartSwitch(props: Props) { function getTopSuggestion( props: Props, visualizationId: string, - newVisualization: Visualization + newVisualization: Visualization, + subVisualizationId?: string ): Suggestion | undefined { const suggestions = getSuggestions({ datasourceMap: props.datasourceMap, @@ -258,6 +268,7 @@ function getTopSuggestion( visualizationMap: { [visualizationId]: newVisualization }, activeVisualizationId: props.visualizationId, visualizationState: props.visualizationState, + subVisualizationId, }).filter(suggestion => { // don't use extended versions of current data table on switching between visualizations // to avoid confusing the user. diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts index eabcdfa7a24ab4..949ae1f43448ed 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts @@ -44,6 +44,7 @@ export function getSuggestions({ datasourceStates, visualizationMap, activeVisualizationId, + subVisualizationId, visualizationState, field, }: { @@ -57,6 +58,7 @@ export function getSuggestions({ >; visualizationMap: Record; activeVisualizationId: string | null; + subVisualizationId?: string; visualizationState: unknown; field?: unknown; }): Suggestion[] { @@ -89,7 +91,8 @@ export function getSuggestions({ table, visualizationId, datasourceSuggestion, - currentVisualizationState + currentVisualizationState, + subVisualizationId ); }) ) @@ -108,13 +111,15 @@ function getVisualizationSuggestions( table: TableSuggestion, visualizationId: string, datasourceSuggestion: DatasourceSuggestion & { datasourceId: string }, - currentVisualizationState: unknown + currentVisualizationState: unknown, + subVisualizationId?: string ) { return visualization .getSuggestions({ table, state: currentVisualizationState, keptLayerIds: datasourceSuggestion.keptLayerIds, + subVisualizationId, }) .map(({ state, ...visualizationSuggestion }) => ({ ...visualizationSuggestion, diff --git a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx index 50cd1ad8bd53a4..e684fe8b3b5d62 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx @@ -28,6 +28,7 @@ export function createMockVisualization(): jest.Mocked { label: 'TEST', }, ], + getVisualizationTypeId: jest.fn(_state => 'empty'), getDescription: jest.fn(_state => ({ label: '' })), switchVisualizationType: jest.fn((_, x) => x), getPersistableState: jest.fn(_state => _state), diff --git a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx index 73b8019a31eaa6..04a1c3865f22d5 100644 --- a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx @@ -53,6 +53,10 @@ export const metricVisualization: Visualization = { }, ], + getVisualizationTypeId() { + return 'lnsMetric'; + }, + clearLayer(state) { return { ...state, diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 181f192520d0dd..ed0af8545f0122 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -312,6 +312,10 @@ export interface SuggestionRequest { * The visualization needs to know which table is being suggested */ keptLayerIds: string[]; + /** + * Different suggestions can be generated for each subtype of the visualization + */ + subVisualizationId?: string; } /** @@ -388,6 +392,11 @@ export interface Visualization { * but can register multiple subtypes */ visualizationTypes: VisualizationType[]; + /** + * Return the ID of the current visualization. Used to highlight + * the active subtype of the visualization. + */ + getVisualizationTypeId: (state: T) => string; /** * If the visualization has subtypes, update the subtype in state. */ diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts index beccf0dc46eb45..d176905c65120b 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts @@ -27,7 +27,7 @@ function exampleState(): State { } describe('xy_visualization', () => { - describe('getDescription', () => { + describe('#getDescription', () => { function mixedState(...types: SeriesType[]) { const state = exampleState(); return { @@ -81,6 +81,45 @@ describe('xy_visualization', () => { }); }); + describe('#getVisualizationTypeId', () => { + function mixedState(...types: SeriesType[]) { + const state = exampleState(); + return { + ...state, + layers: types.map((t, i) => ({ + ...state.layers[0], + layerId: `layer_${i}`, + seriesType: t, + })), + }; + } + + it('should show mixed when each layer is different', () => { + expect(xyVisualization.getVisualizationTypeId(mixedState('bar', 'line'))).toEqual('mixed'); + }); + + it('should show the preferredSeriesType if there are no layers', () => { + expect(xyVisualization.getVisualizationTypeId(mixedState())).toEqual('bar'); + }); + + it('should combine multiple layers into one type', () => { + expect( + xyVisualization.getVisualizationTypeId(mixedState('bar_horizontal', 'bar_horizontal')) + ).toEqual('bar_horizontal'); + }); + + it('should return the subtype for single layers', () => { + expect(xyVisualization.getVisualizationTypeId(mixedState('area'))).toEqual('area'); + expect(xyVisualization.getVisualizationTypeId(mixedState('line'))).toEqual('line'); + expect(xyVisualization.getVisualizationTypeId(mixedState('area_stacked'))).toEqual( + 'area_stacked' + ); + expect(xyVisualization.getVisualizationTypeId(mixedState('bar_horizontal_stacked'))).toEqual( + 'bar_horizontal_stacked' + ); + }); + }); + describe('#initialize', () => { it('loads default state', () => { const mockFrame = createMockFramePublicAPI(); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx index c72fa0fec24d77..e91edf9cc01833 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx @@ -12,7 +12,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { getSuggestions } from './xy_suggestions'; import { LayerContextMenu } from './xy_config_panel'; -import { Visualization, OperationMetadata } from '../types'; +import { Visualization, OperationMetadata, VisualizationType } from '../types'; import { State, PersistableState, SeriesType, visualizationTypes, LayerConfig } from './types'; import { toExpression, toPreviewExpression } from './to_expression'; import chartBarStackedSVG from '../assets/chart_bar_stacked.svg'; @@ -24,6 +24,18 @@ const defaultSeriesType = 'bar_stacked'; const isNumericMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; const isBucketed = (op: OperationMetadata) => op.isBucketed; +function getVisualizationType(state: State): VisualizationType | 'mixed' { + if (!state.layers.length) { + return ( + visualizationTypes.find(t => t.id === state.preferredSeriesType) ?? visualizationTypes[0] + ); + } + const visualizationType = visualizationTypes.find(t => t.id === state.layers[0].seriesType); + const seriesTypes = _.unique(state.layers.map(l => l.seriesType)); + + return visualizationType && seriesTypes.length === 1 ? visualizationType : 'mixed'; +} + function getDescription(state?: State) { if (!state) { return { @@ -34,32 +46,31 @@ function getDescription(state?: State) { }; } + const visualizationType = getVisualizationType(state); + if (!state.layers.length) { - const visualizationType = visualizationTypes.find(v => v.id === state.preferredSeriesType)!; + const preferredType = visualizationType as VisualizationType; return { - icon: visualizationType.largeIcon || visualizationType.icon, - label: visualizationType.label, + icon: preferredType.largeIcon || preferredType.icon, + label: preferredType.label, }; } - const visualizationType = visualizationTypes.find(t => t.id === state.layers[0].seriesType)!; - const seriesTypes = _.unique(state.layers.map(l => l.seriesType)); - return { icon: - seriesTypes.length === 1 - ? visualizationType.largeIcon || visualizationType.icon - : chartMixedSVG, + visualizationType === 'mixed' + ? chartMixedSVG + : visualizationType.largeIcon || visualizationType.icon, label: - seriesTypes.length === 1 - ? visualizationType.label - : isHorizontalChart(state.layers) - ? i18n.translate('xpack.lens.xyVisualization.mixedBarHorizontalLabel', { - defaultMessage: 'Mixed horizontal bar', - }) - : i18n.translate('xpack.lens.xyVisualization.mixedLabel', { - defaultMessage: 'Mixed XY', - }), + visualizationType === 'mixed' + ? isHorizontalChart(state.layers) + ? i18n.translate('xpack.lens.xyVisualization.mixedBarHorizontalLabel', { + defaultMessage: 'Mixed horizontal bar', + }) + : i18n.translate('xpack.lens.xyVisualization.mixedLabel', { + defaultMessage: 'Mixed XY', + }) + : visualizationType.label, }; } @@ -67,6 +78,10 @@ export const xyVisualization: Visualization = { id: 'lnsXY', visualizationTypes, + getVisualizationTypeId(state) { + const type = getVisualizationType(state); + return type === 'mixed' ? type : type.id; + }, getLayerIds(state) { return state.layers.map(l => l.layerId); From 9b65cbd92bb410b008ed8655937cc796369056e3 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Thu, 30 Apr 2020 02:10:14 +0200 Subject: [PATCH 15/30] [Lens] Bind all time fields to the time picker (#63874) * Bind non primary time fields to timepicker * Fix typescript argument types * Allow auto interval on all fields * Remove lens_auto_date function * Fix existing jest tests and add test todos * Remove lens_auto_date from esarchives * Add TimeBuckets jest tests * Fix typo in esarchiver * Address review feedback * Make code a bit better readable * Fix default time field retrieval * Fix TS errors * Add esaggs interpreter tests * Change public API doc of data plugin * Add toExpression tests for index pattern datasource * Add migration stub * Add full migration * Fix naming inconsistency in esaggs * Fix naming issue * Revert archives to un-migrated version * Ignore expressions that are already migrated * test: remove extra spaces and timeField=\\"products.created_on\\"} to timeField=\"products.created_on\"} * Rename all timeField -> timeFields * Combine duplicate functions * Fix boolean error and add test for it * Commit API changes Co-authored-by: Wylie Conlon Co-authored-by: Elastic Machine Co-authored-by: Marta Bondyra --- ...bana-plugin-plugins-data-public.gettime.md | 7 +- ...-data-public.iindexpattern.gettimefield.md | 15 +++ ...lugin-plugins-data-public.iindexpattern.md | 6 + .../kibana-plugin-plugins-data-public.md | 2 +- .../data/common/index_patterns/types.ts | 1 + src/plugins/data/public/public.api.md | 7 +- .../public/query/timefilter/get_time.test.ts | 38 ++++++ .../data/public/query/timefilter/get_time.ts | 27 ++-- .../data/public/query/timefilter/index.ts | 2 +- .../public/query/timefilter/timefilter.ts | 4 +- .../search/aggs/buckets/date_histogram.ts | 2 +- .../data/public/search/expressions/esaggs.ts | 43 +++++- .../data/public/search/tabify/buckets.test.ts | 63 +++++++-- .../data/public/search/tabify/buckets.ts | 12 +- .../data/public/search/tabify/tabify.ts | 18 +-- .../data/public/search/tabify/types.ts | 10 +- src/plugins/data/server/server.api.md | 2 + .../test_suites/run_pipeline/esaggs.ts | 93 +++++++++++++ .../test_suites/run_pipeline/index.ts | 1 + .../indexpattern_datasource/auto_date.test.ts | 83 ------------ .../indexpattern_datasource/auto_date.ts | 79 ------------ .../public/indexpattern_datasource/index.ts | 4 +- .../indexpattern.test.ts | 102 +++++++++++++-- .../indexpattern_datasource/to_expression.ts | 23 ++-- x-pack/plugins/lens/server/migrations.test.ts | 120 +++++++++++++++++ x-pack/plugins/lens/server/migrations.ts | 122 +++++++++++++++++- 26 files changed, 635 insertions(+), 251 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md create mode 100644 test/interpreter_functional/test_suites/run_pipeline/esaggs.ts delete mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts delete mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md index 04a0d871cab2dc..3969a97fa7789f 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md @@ -7,7 +7,10 @@ Signature: ```typescript -export declare function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, forceNow?: Date): import("../..").RangeFilter | undefined; +export declare function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, options?: { + forceNow?: Date; + fieldName?: string; +}): import("../..").RangeFilter | undefined; ``` ## Parameters @@ -16,7 +19,7 @@ export declare function getTime(indexPattern: IIndexPattern | undefined, timeRan | --- | --- | --- | | indexPattern | IIndexPattern | undefined | | | timeRange | TimeRange | | -| forceNow | Date | | +| options | {
forceNow?: Date;
fieldName?: string;
} | | Returns: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md new file mode 100644 index 00000000000000..c3998876c97121 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IIndexPattern](./kibana-plugin-plugins-data-public.iindexpattern.md) > [getTimeField](./kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md) + +## IIndexPattern.getTimeField() method + +Signature: + +```typescript +getTimeField?(): IFieldType | undefined; +``` +Returns: + +`IFieldType | undefined` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md index 1bbd6cf67f0ce3..1cb89822eb605d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md @@ -21,3 +21,9 @@ export interface IIndexPattern | [title](./kibana-plugin-plugins-data-public.iindexpattern.title.md) | string | | | [type](./kibana-plugin-plugins-data-public.iindexpattern.type.md) | string | | +## Methods + +| Method | Description | +| --- | --- | +| [getTimeField()](./kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md) | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 0fd82ffb2240c8..e1df493143b736 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -43,7 +43,7 @@ | [getEsPreference(uiSettings, sessionId)](./kibana-plugin-plugins-data-public.getespreference.md) | | | [getQueryLog(uiSettings, storage, appName, language)](./kibana-plugin-plugins-data-public.getquerylog.md) | | | [getSearchErrorType({ message })](./kibana-plugin-plugins-data-public.getsearcherrortype.md) | | -| [getTime(indexPattern, timeRange, forceNow)](./kibana-plugin-plugins-data-public.gettime.md) | | +| [getTime(indexPattern, timeRange, options)](./kibana-plugin-plugins-data-public.gettime.md) | | | [plugin(initializerContext)](./kibana-plugin-plugins-data-public.plugin.md) | | ## Interfaces diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 698edbf9cd6a8e..e21d27a70e02ad 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -26,6 +26,7 @@ export interface IIndexPattern { id?: string; type?: string; timeFieldName?: string; + getTimeField?(): IFieldType | undefined; fieldFormatMap?: Record< string, { diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 86560b3ccf7b12..91dea66f06a94b 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -699,7 +699,10 @@ export function getSearchErrorType({ message }: Pick): " // Warning: (ae-missing-release-tag) "getTime" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, forceNow?: Date): import("../..").RangeFilter | undefined; +export function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, options?: { + forceNow?: Date; + fieldName?: string; +}): import("../..").RangeFilter | undefined; // Warning: (ae-missing-release-tag) "IAggConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -842,6 +845,8 @@ export interface IIndexPattern { // (undocumented) fields: IFieldType[]; // (undocumented) + getTimeField?(): IFieldType | undefined; + // (undocumented) id?: string; // (undocumented) timeFieldName?: string; diff --git a/src/plugins/data/public/query/timefilter/get_time.test.ts b/src/plugins/data/public/query/timefilter/get_time.test.ts index a8eb3a3fe81023..4dba157a6f5546 100644 --- a/src/plugins/data/public/query/timefilter/get_time.test.ts +++ b/src/plugins/data/public/query/timefilter/get_time.test.ts @@ -51,5 +51,43 @@ describe('get_time', () => { }); clock.restore(); }); + + test('build range filter for non-primary field', () => { + const clock = sinon.useFakeTimers(moment.utc([2000, 1, 1, 0, 0, 0, 0]).valueOf()); + + const filter = getTime( + { + id: 'test', + title: 'test', + timeFieldName: 'date', + fields: [ + { + name: 'date', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + filterable: true, + }, + { + name: 'myCustomDate', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + filterable: true, + }, + ], + } as any, + { from: 'now-60y', to: 'now' }, + { fieldName: 'myCustomDate' } + ); + expect(filter!.range.myCustomDate).toEqual({ + gte: '1940-02-01T00:00:00.000Z', + lte: '2000-02-01T00:00:00.000Z', + format: 'strict_date_optional_time', + }); + clock.restore(); + }); }); }); diff --git a/src/plugins/data/public/query/timefilter/get_time.ts b/src/plugins/data/public/query/timefilter/get_time.ts index fa154061890412..9cdd25d3213cee 100644 --- a/src/plugins/data/public/query/timefilter/get_time.ts +++ b/src/plugins/data/public/query/timefilter/get_time.ts @@ -19,7 +19,7 @@ import dateMath from '@elastic/datemath'; import { IIndexPattern } from '../..'; -import { TimeRange, IFieldType, buildRangeFilter } from '../../../common'; +import { TimeRange, buildRangeFilter } from '../../../common'; interface CalculateBoundsOptions { forceNow?: Date; @@ -35,18 +35,27 @@ export function calculateBounds(timeRange: TimeRange, options: CalculateBoundsOp export function getTime( indexPattern: IIndexPattern | undefined, timeRange: TimeRange, + options?: { forceNow?: Date; fieldName?: string } +) { + return createTimeRangeFilter( + indexPattern, + timeRange, + options?.fieldName || indexPattern?.timeFieldName, + options?.forceNow + ); +} + +function createTimeRangeFilter( + indexPattern: IIndexPattern | undefined, + timeRange: TimeRange, + fieldName?: string, forceNow?: Date ) { if (!indexPattern) { - // in CI, we sometimes seem to fail here. return; } - - const timefield: IFieldType | undefined = indexPattern.fields.find( - field => field.name === indexPattern.timeFieldName - ); - - if (!timefield) { + const field = indexPattern.fields.find(f => f.name === (fieldName || indexPattern.timeFieldName)); + if (!field) { return; } @@ -55,7 +64,7 @@ export function getTime( return; } return buildRangeFilter( - timefield, + field, { ...(bounds.min && { gte: bounds.min.toISOString() }), ...(bounds.max && { lte: bounds.max.toISOString() }), diff --git a/src/plugins/data/public/query/timefilter/index.ts b/src/plugins/data/public/query/timefilter/index.ts index a6260e782c12ff..034af03842ab8d 100644 --- a/src/plugins/data/public/query/timefilter/index.ts +++ b/src/plugins/data/public/query/timefilter/index.ts @@ -22,6 +22,6 @@ export { TimefilterService, TimefilterSetup } from './timefilter_service'; export * from './types'; export { Timefilter, TimefilterContract } from './timefilter'; export { TimeHistory, TimeHistoryContract } from './time_history'; -export { getTime } from './get_time'; +export { getTime, calculateBounds } from './get_time'; export { changeTimeFilter } from './lib/change_time_filter'; export { extractTimeFilter } from './lib/extract_time_filter'; diff --git a/src/plugins/data/public/query/timefilter/timefilter.ts b/src/plugins/data/public/query/timefilter/timefilter.ts index 4fbdac47fb3b02..86ef69be572a9c 100644 --- a/src/plugins/data/public/query/timefilter/timefilter.ts +++ b/src/plugins/data/public/query/timefilter/timefilter.ts @@ -164,7 +164,9 @@ export class Timefilter { }; public createFilter = (indexPattern: IndexPattern, timeRange?: TimeRange) => { - return getTime(indexPattern, timeRange ? timeRange : this._time, this.getForceNow()); + return getTime(indexPattern, timeRange ? timeRange : this._time, { + forceNow: this.getForceNow(), + }); }; public getBounds(): TimeRangeBounds { diff --git a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts b/src/plugins/data/public/search/aggs/buckets/date_histogram.ts index 57f3aa85ad9443..3ecdc17cb57f3f 100644 --- a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts +++ b/src/plugins/data/public/search/aggs/buckets/date_histogram.ts @@ -45,7 +45,7 @@ const updateTimeBuckets = ( customBuckets?: IBucketDateHistogramAggConfig['buckets'] ) => { const bounds = - agg.params.timeRange && agg.fieldIsTimeField() + agg.params.timeRange && (agg.fieldIsTimeField() || agg.params.interval === 'auto') ? timefilter.calculateBounds(agg.params.timeRange) : undefined; const buckets = customBuckets || agg.buckets; diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index 087b83127079f9..eec75b08411330 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -32,8 +32,15 @@ import { Adapters } from '../../../../../plugins/inspector/public'; import { IAggConfigs } from '../aggs'; import { ISearchSource } from '../search_source'; import { tabifyAggResponse } from '../tabify'; -import { Filter, Query, serializeFieldFormat, TimeRange } from '../../../common'; -import { FilterManager, getTime } from '../../query'; +import { + Filter, + Query, + serializeFieldFormat, + TimeRange, + IIndexPattern, + isRangeFilter, +} from '../../../common'; +import { FilterManager, calculateBounds, getTime } from '../../query'; import { getSearchService, getQueryService, getIndexPatterns } from '../../services'; import { buildTabularInspectorData } from './build_tabular_inspector_data'; import { getRequestInspectorStats, getResponseInspectorStats, serializeAggConfig } from './utils'; @@ -42,6 +49,8 @@ export interface RequestHandlerParams { searchSource: ISearchSource; aggs: IAggConfigs; timeRange?: TimeRange; + timeFields?: string[]; + indexPattern?: IIndexPattern; query?: Query; filters?: Filter[]; forceFetch: boolean; @@ -65,12 +74,15 @@ interface Arguments { partialRows: boolean; includeFormatHints: boolean; aggConfigs: string; + timeFields?: string[]; } const handleCourierRequest = async ({ searchSource, aggs, timeRange, + timeFields, + indexPattern, query, filters, forceFetch, @@ -111,9 +123,19 @@ const handleCourierRequest = async ({ return aggs.onSearchRequestStart(paramSearchSource, options); }); - if (timeRange) { + // If timeFields have been specified, use the specified ones, otherwise use primary time field of index + // pattern if it's available. + const defaultTimeField = indexPattern?.getTimeField?.(); + const defaultTimeFields = defaultTimeField ? [defaultTimeField.name] : []; + const allTimeFields = timeFields && timeFields.length > 0 ? timeFields : defaultTimeFields; + + // If a timeRange has been specified and we had at least one timeField available, create range + // filters for that those time fields + if (timeRange && allTimeFields.length > 0) { timeFilterSearchSource.setField('filter', () => { - return getTime(searchSource.getField('index'), timeRange); + return allTimeFields + .map(fieldName => getTime(indexPattern, timeRange, { fieldName })) + .filter(isRangeFilter); }); } @@ -181,11 +203,13 @@ const handleCourierRequest = async ({ (searchSource as any).finalResponse = resp; - const parsedTimeRange = timeRange ? getTime(aggs.indexPattern, timeRange) : null; + const parsedTimeRange = timeRange ? calculateBounds(timeRange) : null; const tabifyParams = { metricsAtAllLevels, partialRows, - timeRange: parsedTimeRange ? parsedTimeRange.range : undefined, + timeRange: parsedTimeRange + ? { from: parsedTimeRange.min, to: parsedTimeRange.max, timeFields: allTimeFields } + : undefined, }; const tabifyCacheHash = calculateObjectHash({ tabifyAggs: aggs, ...tabifyParams }); @@ -242,6 +266,11 @@ export const esaggs = (): ExpressionFunctionDefinition { const check = (aggResp: any, count: number, keys: string[]) => { @@ -187,9 +192,9 @@ describe('Buckets wrapper', () => { }, }; const timeRange = { - gte: 150, - lte: 350, - name: 'date', + from: moment(150), + to: moment(350), + timeFields: ['date'], }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); @@ -204,9 +209,9 @@ describe('Buckets wrapper', () => { }, }; const timeRange = { - gte: 150, - lte: 350, - name: 'date', + from: moment(150), + to: moment(350), + timeFields: ['date'], }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); @@ -221,9 +226,9 @@ describe('Buckets wrapper', () => { }, }; const timeRange = { - gte: 100, - lte: 400, - name: 'date', + from: moment(100), + to: moment(400), + timeFields: ['date'], }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); @@ -238,13 +243,47 @@ describe('Buckets wrapper', () => { }, }; const timeRange = { - gte: 150, - lte: 350, - name: 'date', + from: moment(150), + to: moment(350), + timeFields: ['date'], }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); expect(buckets).toHaveLength(4); }); + + test('does drop bucket when multiple time fields specified', () => { + const aggParams = { + drop_partials: true, + field: { + name: 'date', + }, + }; + const timeRange = { + from: moment(100), + to: moment(350), + timeFields: ['date', 'other_datefield'], + }; + const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); + + expect(buckets.buckets.map((b: Bucket) => b.key)).toEqual([100, 200]); + }); + + test('does not drop bucket when no timeFields have been specified', () => { + const aggParams = { + drop_partials: true, + field: { + name: 'date', + }, + }; + const timeRange = { + from: moment(100), + to: moment(350), + timeFields: [], + }; + const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); + + expect(buckets.buckets.map((b: Bucket) => b.key)).toEqual([0, 100, 200, 300]); + }); }); }); diff --git a/src/plugins/data/public/search/tabify/buckets.ts b/src/plugins/data/public/search/tabify/buckets.ts index 971e820ac6ddf6..cd52a09caeaade 100644 --- a/src/plugins/data/public/search/tabify/buckets.ts +++ b/src/plugins/data/public/search/tabify/buckets.ts @@ -20,7 +20,7 @@ import { get, isPlainObject, keys, findKey } from 'lodash'; import moment from 'moment'; import { IAggConfig } from '../aggs'; -import { AggResponseBucket, TabbedRangeFilterParams } from './types'; +import { AggResponseBucket, TabbedRangeFilterParams, TimeRangeInformation } from './types'; type AggParams = IAggConfig['params'] & { drop_partials: boolean; @@ -36,7 +36,7 @@ export class TabifyBuckets { buckets: any; _keys: any[] = []; - constructor(aggResp: any, aggParams?: AggParams, timeRange?: TabbedRangeFilterParams) { + constructor(aggResp: any, aggParams?: AggParams, timeRange?: TimeRangeInformation) { if (aggResp && aggResp.buckets) { this.buckets = aggResp.buckets; } else if (aggResp) { @@ -107,12 +107,12 @@ export class TabifyBuckets { // dropPartials should only be called if the aggParam setting is enabled, // and the agg field is the same as the Time Range. - private dropPartials(params: AggParams, timeRange?: TabbedRangeFilterParams) { + private dropPartials(params: AggParams, timeRange?: TimeRangeInformation) { if ( !timeRange || this.buckets.length <= 1 || this.objectMode || - params.field.name !== timeRange.name + !timeRange.timeFields.includes(params.field.name) ) { return; } @@ -120,10 +120,10 @@ export class TabifyBuckets { const interval = this.buckets[1].key - this.buckets[0].key; this.buckets = this.buckets.filter((bucket: AggResponseBucket) => { - if (moment(bucket.key).isBefore(timeRange.gte)) { + if (moment(bucket.key).isBefore(timeRange.from)) { return false; } - if (moment(bucket.key + interval).isAfter(timeRange.lte)) { + if (moment(bucket.key + interval).isAfter(timeRange.to)) { return false; } return true; diff --git a/src/plugins/data/public/search/tabify/tabify.ts b/src/plugins/data/public/search/tabify/tabify.ts index e93e9890342529..9cb55f94537c56 100644 --- a/src/plugins/data/public/search/tabify/tabify.ts +++ b/src/plugins/data/public/search/tabify/tabify.ts @@ -20,7 +20,7 @@ import { get } from 'lodash'; import { TabbedAggResponseWriter } from './response_writer'; import { TabifyBuckets } from './buckets'; -import { TabbedResponseWriterOptions, TabbedRangeFilterParams } from './types'; +import { TabbedResponseWriterOptions } from './types'; import { AggResponseBucket } from './types'; import { AggGroupNames, IAggConfigs } from '../aggs'; @@ -54,7 +54,7 @@ export function tabifyAggResponse( switch (agg.type.type) { case AggGroupNames.Buckets: const aggBucket = get(bucket, agg.id); - const tabifyBuckets = new TabifyBuckets(aggBucket, agg.params, timeRange); + const tabifyBuckets = new TabifyBuckets(aggBucket, agg.params, respOpts?.timeRange); if (tabifyBuckets.length) { tabifyBuckets.forEach((subBucket, tabifyBucketKey) => { @@ -153,20 +153,6 @@ export function tabifyAggResponse( doc_count: esResponse.hits.total, }; - let timeRange: TabbedRangeFilterParams | undefined; - - // Extract the time range object if provided - if (respOpts && respOpts.timeRange) { - const [timeRangeKey] = Object.keys(respOpts.timeRange); - - if (timeRangeKey) { - timeRange = { - name: timeRangeKey, - ...respOpts.timeRange[timeRangeKey], - }; - } - } - collectBucket(aggConfigs, write, topLevelBucket, '', 1); return write.response(); diff --git a/src/plugins/data/public/search/tabify/types.ts b/src/plugins/data/public/search/tabify/types.ts index 1e051880d3f19f..72e91eb58c8a97 100644 --- a/src/plugins/data/public/search/tabify/types.ts +++ b/src/plugins/data/public/search/tabify/types.ts @@ -17,6 +17,7 @@ * under the License. */ +import { Moment } from 'moment'; import { RangeFilterParams } from '../../../common'; import { IAggConfig } from '../aggs'; @@ -25,11 +26,18 @@ export interface TabbedRangeFilterParams extends RangeFilterParams { name: string; } +/** @internal */ +export interface TimeRangeInformation { + from?: Moment; + to?: Moment; + timeFields: string[]; +} + /** @internal **/ export interface TabbedResponseWriterOptions { metricsAtAllLevels: boolean; partialRows: boolean; - timeRange?: { [key: string]: RangeFilterParams }; + timeRange?: TimeRangeInformation; } /** @internal */ diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 5d94b6516c2bab..df4ba23244b4dc 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -408,6 +408,8 @@ export interface IIndexPattern { // (undocumented) fields: IFieldType[]; // (undocumented) + getTimeField?(): IFieldType | undefined; + // (undocumented) id?: string; // (undocumented) timeFieldName?: string; diff --git a/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts b/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts new file mode 100644 index 00000000000000..5ea151dffdc8ef --- /dev/null +++ b/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { ExpectExpression, expectExpressionProvider } from './helpers'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; + +function getCell(esaggsResult: any, column: number, row: number): unknown | undefined { + const columnId = esaggsResult?.columns[column]?.id; + if (!columnId) { + return; + } + return esaggsResult?.rows[row]?.[columnId]; +} + +export default function({ + getService, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { + let expectExpression: ExpectExpression; + describe('esaggs pipeline expression tests', () => { + before(() => { + expectExpression = expectExpressionProvider({ getService, updateBaselines }); + }); + + describe('correctly renders tagcloud', () => { + it('filters on index pattern primary date field by default', async () => { + const aggConfigs = [{ id: 1, enabled: true, type: 'count', schema: 'metric', params: {} }]; + const timeRange = { + from: '2006-09-21T00:00:00Z', + to: '2015-09-22T00:00:00Z', + }; + const expression = ` + kibana_context timeRange='${JSON.stringify(timeRange)}' + | esaggs index='logstash-*' aggConfigs='${JSON.stringify(aggConfigs)}' + `; + const result = await expectExpression('esaggs_primary_timefield', expression).getResponse(); + expect(getCell(result, 0, 0)).to.be(9375); + }); + + it('filters on the specified date field', async () => { + const aggConfigs = [{ id: 1, enabled: true, type: 'count', schema: 'metric', params: {} }]; + const timeRange = { + from: '2006-09-21T00:00:00Z', + to: '2015-09-22T00:00:00Z', + }; + const expression = ` + kibana_context timeRange='${JSON.stringify(timeRange)}' + | esaggs index='logstash-*' timeFields='relatedContent.article:published_time' aggConfigs='${JSON.stringify( + aggConfigs + )}' + `; + const result = await expectExpression('esaggs_other_timefield', expression).getResponse(); + expect(getCell(result, 0, 0)).to.be(11134); + }); + + it('filters on multiple specified date field', async () => { + const aggConfigs = [{ id: 1, enabled: true, type: 'count', schema: 'metric', params: {} }]; + const timeRange = { + from: '2006-09-21T00:00:00Z', + to: '2015-09-22T00:00:00Z', + }; + const expression = ` + kibana_context timeRange='${JSON.stringify(timeRange)}' + | esaggs index='logstash-*' timeFields='relatedContent.article:published_time' timeFields='@timestamp' aggConfigs='${JSON.stringify( + aggConfigs + )}' + `; + const result = await expectExpression( + 'esaggs_multiple_timefields', + expression + ).getResponse(); + expect(getCell(result, 0, 0)).to.be(7452); + }); + }); + }); +} diff --git a/test/interpreter_functional/test_suites/run_pipeline/index.ts b/test/interpreter_functional/test_suites/run_pipeline/index.ts index 031a0e3576ccc0..9590f9f8c17940 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/index.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/index.ts @@ -46,5 +46,6 @@ export default function({ getService, getPageObjects, loadTestFile }: FtrProvide loadTestFile(require.resolve('./basic')); loadTestFile(require.resolve('./tag_cloud')); loadTestFile(require.resolve('./metric')); + loadTestFile(require.resolve('./esaggs')); }); } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts deleted file mode 100644 index 5f35ef650a08c2..00000000000000 --- a/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; -import { getAutoDate } from './auto_date'; - -describe('auto_date', () => { - let autoDate: ReturnType; - - beforeEach(() => { - autoDate = getAutoDate({ data: dataPluginMock.createSetupContract() }); - }); - - it('should do nothing if no time range is provided', () => { - const result = autoDate.fn( - { - type: 'kibana_context', - }, - { - aggConfigs: 'canttouchthis', - }, - // eslint-disable-next-line - {} as any - ); - - expect(result).toEqual('canttouchthis'); - }); - - it('should not change anything if there are no auto date histograms', () => { - const aggConfigs = JSON.stringify([ - { type: 'date_histogram', params: { interval: '35h' } }, - { type: 'count' }, - ]); - const result = autoDate.fn( - { - timeRange: { - from: 'now-10d', - to: 'now', - }, - type: 'kibana_context', - }, - { - aggConfigs, - }, - // eslint-disable-next-line - {} as any - ); - - expect(result).toEqual(aggConfigs); - }); - - it('should change auto date histograms', () => { - const aggConfigs = JSON.stringify([ - { type: 'date_histogram', params: { interval: 'auto' } }, - { type: 'count' }, - ]); - const result = autoDate.fn( - { - timeRange: { - from: 'now-10d', - to: 'now', - }, - type: 'kibana_context', - }, - { - aggConfigs, - }, - // eslint-disable-next-line - {} as any - ); - - const interval = JSON.parse(result).find( - (agg: { type: string }) => agg.type === 'date_histogram' - ).params.interval; - - expect(interval).toBeTruthy(); - expect(typeof interval).toEqual('string'); - expect(interval).not.toEqual('auto'); - }); -}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts b/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts deleted file mode 100644 index 97a46f4a3e1765..00000000000000 --- a/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public'; -import { - ExpressionFunctionDefinition, - KibanaContext, -} from '../../../../../src/plugins/expressions/public'; - -interface LensAutoDateProps { - aggConfigs: string; -} - -export function getAutoDate(deps: { - data: DataPublicPluginSetup; -}): ExpressionFunctionDefinition< - 'lens_auto_date', - KibanaContext | null, - LensAutoDateProps, - string -> { - function autoIntervalFromContext(ctx?: KibanaContext | null) { - if (!ctx || !ctx.timeRange) { - return; - } - - return deps.data.search.aggs.calculateAutoTimeExpression(ctx.timeRange); - } - - /** - * Convert all 'auto' date histograms into a concrete value (e.g. 2h). - * This allows us to support 'auto' on all date fields, and opens the - * door to future customizations (e.g. adjusting the level of detail, etc). - */ - return { - name: 'lens_auto_date', - aliases: [], - help: '', - inputTypes: ['kibana_context', 'null'], - args: { - aggConfigs: { - types: ['string'], - default: '""', - help: '', - }, - }, - fn(input, args) { - const interval = autoIntervalFromContext(input); - - if (!interval) { - return args.aggConfigs; - } - - const configs = JSON.parse(args.aggConfigs) as Array<{ - type: string; - params: { interval: string }; - }>; - - const updatedConfigs = configs.map(c => { - if (c.type !== 'date_histogram' || !c.params || c.params.interval !== 'auto') { - return c; - } - - return { - ...c, - params: { - ...c.params, - interval, - }, - }; - }); - - return JSON.stringify(updatedConfigs); - }, - }; -} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts index fe14f472341afd..73fd144b9c7f87 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts @@ -8,7 +8,6 @@ import { CoreSetup } from 'kibana/public'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { getIndexPatternDatasource } from './indexpattern'; import { renameColumns } from './rename_columns'; -import { getAutoDate } from './auto_date'; import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; import { DataPublicPluginSetup, @@ -31,10 +30,9 @@ export class IndexPatternDatasource { setup( core: CoreSetup, - { data: dataSetup, expressions, editorFrame }: IndexPatternDatasourceSetupPlugins + { expressions, editorFrame }: IndexPatternDatasourceSetupPlugins ) { expressions.registerFunction(renameColumns); - expressions.registerFunction(getAutoDate({ data: dataSetup })); editorFrame.registerDatasource( core.getStartServices().then(([coreStart, { data }]) => diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index e4f3677d0fe88e..06635e663361db 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -10,6 +10,7 @@ import { DatasourcePublicAPI, Operation, Datasource } from '../types'; import { coreMock } from 'src/core/public/mocks'; import { IndexPatternPersistedState, IndexPatternPrivateState } from './types'; import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; +import { Ast } from '@kbn/interpreter/common'; jest.mock('./loader'); jest.mock('../id_generator'); @@ -262,20 +263,7 @@ describe('IndexPattern Data Source', () => { Object { "arguments": Object { "aggConfigs": Array [ - Object { - "chain": Array [ - Object { - "arguments": Object { - "aggConfigs": Array [ - "[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1d\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}}]", - ], - }, - "function": "lens_auto_date", - "type": "function", - }, - ], - "type": "expression", - }, + "[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1d\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}}]", ], "includeFormatHints": Array [ true, @@ -289,6 +277,9 @@ describe('IndexPattern Data Source', () => { "partialRows": Array [ false, ], + "timeFields": Array [ + "timestamp", + ], }, "function": "esaggs", "type": "function", @@ -307,6 +298,89 @@ describe('IndexPattern Data Source', () => { } `); }); + + it('should put all time fields used in date_histograms to the esaggs timeFields parameter', async () => { + const queryPersistedState: IndexPatternPersistedState = { + currentIndexPatternId: '1', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: { + label: 'Count of records', + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + col2: { + label: 'Date', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { + interval: 'auto', + }, + }, + col3: { + label: 'Date 2', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'another_datefield', + params: { + interval: 'auto', + }, + }, + }, + }, + }, + }; + + const state = stateFromPersistedState(queryPersistedState); + + const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp', 'another_datefield']); + }); + + it('should not put date fields used outside date_histograms to the esaggs timeFields parameter', async () => { + const queryPersistedState: IndexPatternPersistedState = { + currentIndexPatternId: '1', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + label: 'Count of records', + dataType: 'date', + isBucketed: false, + sourceField: 'timefield', + operationType: 'cardinality', + }, + col2: { + label: 'Date', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { + interval: 'auto', + }, + }, + }, + }, + }, + }; + + const state = stateFromPersistedState(queryPersistedState); + + const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp']); + expect(ast.chain[0].arguments.timeFields).not.toContain('timefield'); + }); }); describe('#insertLayer', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts index 3ab51b5fa3f2b4..1308fa3b7ca603 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -10,6 +10,7 @@ import { IndexPatternColumn } from './indexpattern'; import { operationDefinitionMap } from './operations'; import { IndexPattern, IndexPatternPrivateState } from './types'; import { OriginalColumn } from './rename_columns'; +import { dateHistogramOperation } from './operations/definitions'; function getExpressionForLayer( indexPattern: IndexPattern, @@ -68,6 +69,12 @@ function getExpressionForLayer( return base; }); + const allDateHistogramFields = Object.values(columns) + .map(column => + column.operationType === dateHistogramOperation.type ? column.sourceField : null + ) + .filter((field): field is string => Boolean(field)); + return { type: 'expression', chain: [ @@ -79,20 +86,8 @@ function getExpressionForLayer( metricsAtAllLevels: [false], partialRows: [false], includeFormatHints: [true], - aggConfigs: [ - { - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_auto_date', - arguments: { - aggConfigs: [JSON.stringify(aggs)], - }, - }, - ], - }, - ], + timeFields: allDateHistogramFields, + aggConfigs: [JSON.stringify(aggs)], }, }, { diff --git a/x-pack/plugins/lens/server/migrations.test.ts b/x-pack/plugins/lens/server/migrations.test.ts index e80308cc9acdb0..4cc330d40efd7f 100644 --- a/x-pack/plugins/lens/server/migrations.test.ts +++ b/x-pack/plugins/lens/server/migrations.test.ts @@ -158,4 +158,124 @@ describe('Lens migrations', () => { ]); }); }); + + describe('7.8.0 auto timestamp', () => { + const context = {} as SavedObjectMigrationContext; + + const example = { + type: 'lens', + attributes: { + expression: `kibana + | kibana_context query="{\\"query\\":\\"\\",\\"language\\":\\"kuery\\"}" filters="[]" + | lens_merge_tables layerIds="bd09dc71-a7e2-42d0-83bd-85df8291f03c" + tables={esaggs + index="ff959d40-b880-11e8-a6d9-e546fe2bba5f" + metricsAtAllLevels=false + partialRows=false + includeFormatHints=true + aggConfigs={ + lens_auto_date + aggConfigs="[{\\"id\\":\\"1d9cc16c-1460-41de-88f8-471932ecbc97\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"products.created_on\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"auto\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}},{\\"id\\":\\"66115819-8481-4917-a6dc-8ffb10dd02df\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}}]" + } + | lens_rename_columns idMap="{\\"col-0-1d9cc16c-1460-41de-88f8-471932ecbc97\\":{\\"label\\":\\"products.created_on\\",\\"dataType\\":\\"date\\",\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"products.created_on\\",\\"isBucketed\\":true,\\"scale\\":\\"interval\\",\\"params\\":{\\"interval\\":\\"auto\\"},\\"id\\":\\"1d9cc16c-1460-41de-88f8-471932ecbc97\\"},\\"col-1-66115819-8481-4917-a6dc-8ffb10dd02df\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"operationType\\":\\"count\\",\\"suggestedPriority\\":0,\\"isBucketed\\":false,\\"scale\\":\\"ratio\\",\\"sourceField\\":\\"Records\\",\\"id\\":\\"66115819-8481-4917-a6dc-8ffb10dd02df\\"}}" + } + | lens_xy_chart + xTitle="products.created_on" + yTitle="Count of records" + legend={lens_xy_legendConfig isVisible=true position="right"} + layers={lens_xy_layer + layerId="bd09dc71-a7e2-42d0-83bd-85df8291f03c" + hide=false + xAccessor="1d9cc16c-1460-41de-88f8-471932ecbc97" + yScaleType="linear" + xScaleType="time" + isHistogram=true + seriesType="bar_stacked" + accessors="66115819-8481-4917-a6dc-8ffb10dd02df" + columnToLabel="{\\"66115819-8481-4917-a6dc-8ffb10dd02df\\":\\"Count of records\\"}" + } + `, + state: { + datasourceStates: { + indexpattern: { + currentIndexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + layers: { + 'bd09dc71-a7e2-42d0-83bd-85df8291f03c': { + indexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + columns: { + '1d9cc16c-1460-41de-88f8-471932ecbc97': { + label: 'products.created_on', + dataType: 'date', + operationType: 'date_histogram', + sourceField: 'products.created_on', + isBucketed: true, + scale: 'interval', + params: { interval: 'auto' }, + }, + '66115819-8481-4917-a6dc-8ffb10dd02df': { + label: 'Count of records', + dataType: 'number', + operationType: 'count', + suggestedPriority: 0, + isBucketed: false, + scale: 'ratio', + sourceField: 'Records', + }, + }, + columnOrder: [ + '1d9cc16c-1460-41de-88f8-471932ecbc97', + '66115819-8481-4917-a6dc-8ffb10dd02df', + ], + }, + }, + }, + }, + datasourceMetaData: { + filterableIndexPatterns: [ + { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', title: 'kibana_sample_data_ecommerce' }, + ], + }, + visualization: { + legend: { isVisible: true, position: 'right' }, + preferredSeriesType: 'bar_stacked', + layers: [ + { + layerId: 'bd09dc71-a7e2-42d0-83bd-85df8291f03c', + accessors: ['66115819-8481-4917-a6dc-8ffb10dd02df'], + position: 'top', + seriesType: 'bar_stacked', + showGridlines: false, + xAccessor: '1d9cc16c-1460-41de-88f8-471932ecbc97', + }, + ], + }, + query: { query: '', language: 'kuery' }, + filters: [], + }, + title: 'Bar chart', + visualizationType: 'lnsXY', + }, + }; + + it('should remove the lens_auto_date expression', () => { + const result = migrations['7.8.0'](example, context); + expect(result.attributes.expression).toContain(`timeFields=\"products.created_on\"`); + }); + + it('should handle pre-migrated expression', () => { + const input = { + type: 'lens', + attributes: { + ...example.attributes, + expression: `kibana +| kibana_context query="{\\"query\\":\\"\\",\\"language\\":\\"kuery\\"}" filters="[]" +| lens_merge_tables layerIds="bd09dc71-a7e2-42d0-83bd-85df8291f03c" + tables={esaggs index="ff959d40-b880-11e8-a6d9-e546fe2bba5f" metricsAtAllLevels=false partialRows=false includeFormatHints=true aggConfigs="[{\\"id\\":\\"1d9cc16c-1460-41de-88f8-471932ecbc97\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"products.created_on\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"auto\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}},{\\"id\\":\\"66115819-8481-4917-a6dc-8ffb10dd02df\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}}]" timeFields=\"products.created_on\"} +| lens_xy_chart xTitle="products.created_on" yTitle="Count of records" legend={lens_xy_legendConfig isVisible=true position="right"} layers={}`, + }, + }; + const result = migrations['7.8.0'](input, context); + expect(result).toEqual(input); + }); + }); }); diff --git a/x-pack/plugins/lens/server/migrations.ts b/x-pack/plugins/lens/server/migrations.ts index 3d238723b7438c..51fcd3b6198c3b 100644 --- a/x-pack/plugins/lens/server/migrations.ts +++ b/x-pack/plugins/lens/server/migrations.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { cloneDeep } from 'lodash'; +import { cloneDeep, flow } from 'lodash'; +import { fromExpression, toExpression, Ast, ExpressionFunctionAST } from '@kbn/interpreter/common'; import { SavedObjectMigrationFn } from 'src/core/server'; interface XYLayerPre77 { @@ -14,6 +15,122 @@ interface XYLayerPre77 { accessors: string[]; } +/** + * Removes the `lens_auto_date` subexpression from a stored expression + * string. For example: aggConfigs={lens_auto_date aggConfigs="JSON string"} + */ +const removeLensAutoDate: SavedObjectMigrationFn = (doc, context) => { + const expression: string = doc.attributes?.expression; + try { + const ast = fromExpression(expression); + const newChain: ExpressionFunctionAST[] = ast.chain.map(topNode => { + if (topNode.function !== 'lens_merge_tables') { + return topNode; + } + return { + ...topNode, + arguments: { + ...topNode.arguments, + tables: (topNode.arguments.tables as Ast[]).map(middleNode => { + return { + type: 'expression', + chain: middleNode.chain.map(node => { + // Check for sub-expression in aggConfigs + if ( + node.function === 'esaggs' && + typeof node.arguments.aggConfigs[0] !== 'string' + ) { + return { + ...node, + arguments: { + ...node.arguments, + aggConfigs: (node.arguments.aggConfigs[0] as Ast).chain[0].arguments + .aggConfigs, + }, + }; + } + return node; + }), + }; + }), + }, + }; + }); + + return { + ...doc, + attributes: { + ...doc.attributes, + expression: toExpression({ ...ast, chain: newChain }), + }, + }; + } catch (e) { + context.log.warning(e.message); + return { ...doc }; + } +}; + +/** + * Adds missing timeField arguments to esaggs in the Lens expression + */ +const addTimeFieldToEsaggs: SavedObjectMigrationFn = (doc, context) => { + const expression: string = doc.attributes?.expression; + + try { + const ast = fromExpression(expression); + const newChain: ExpressionFunctionAST[] = ast.chain.map(topNode => { + if (topNode.function !== 'lens_merge_tables') { + return topNode; + } + return { + ...topNode, + arguments: { + ...topNode.arguments, + tables: (topNode.arguments.tables as Ast[]).map(middleNode => { + return { + type: 'expression', + chain: middleNode.chain.map(node => { + // Skip if there are any timeField arguments already, because that indicates + // the fix is already applied + if (node.function !== 'esaggs' || node.arguments.timeFields) { + return node; + } + const timeFields: string[] = []; + JSON.parse(node.arguments.aggConfigs[0] as string).forEach( + (agg: { type: string; params: { field: string } }) => { + if (agg.type !== 'date_histogram') { + return; + } + timeFields.push(agg.params.field); + } + ); + return { + ...node, + arguments: { + ...node.arguments, + timeFields, + }, + }; + }), + }; + }), + }, + }; + }); + + return { + ...doc, + attributes: { + ...doc.attributes, + expression: toExpression({ ...ast, chain: newChain }), + }, + }; + } catch (e) { + context.log.warning(e.message); + return { ...doc }; + } +}; + export const migrations: Record = { '7.7.0': doc => { const newDoc = cloneDeep(doc); @@ -34,4 +151,7 @@ export const migrations: Record = { } return newDoc; }, + // The order of these migrations matter, since the timefield migration relies on the aggConfigs + // sitting directly on the esaggs as an argument and not a nested function (which lens_auto_date was). + '7.8.0': flow(removeLensAutoDate, addTimeFieldToEsaggs), }; From fba5128bd85d66c346abd1d3762ac6c57cd4a777 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Wed, 29 Apr 2020 18:03:30 -0700 Subject: [PATCH 16/30] [Ingest] Edit datasource UI (#64727) * Adjust NewDatasource type to exclude stream `agent_stream` property, add additional datasource hooks * Initial pass at edit datasource UI * Clean up dupe code, fix submit button not enabled after re-selecting a package * Remove delete config functionality from list page * Show validation errors for data source name and description fields * Fix types * Add success toasts * Review fixes, clean up i18n Co-authored-by: Elastic Machine --- .../datasource_to_agent_datasource.test.ts | 21 +- .../datasource_to_agent_datasource.ts | 8 +- .../common/types/models/agent_config.ts | 4 +- .../common/types/models/datasource.ts | 18 +- .../hooks/use_request/datasource.ts | 26 +- .../components/layout.tsx | 24 +- .../create_datasource_page/index.tsx | 26 +- .../step_configure_datasource.tsx | 62 +--- .../step_define_datasource.tsx | 13 +- .../create_datasource_page/types.ts | 3 +- .../datasources/datasources_table.tsx | 44 +-- .../edit_datasource_page/index.tsx | 323 ++++++++++++++++++ .../sections/agent_config/index.tsx | 4 + .../sections/agent_config/list_page/index.tsx | 69 +--- .../ingest_manager/types/index.ts | 2 + .../server/routes/agent_config/handlers.ts | 3 +- .../translations/translations/ja-JP.json | 5 - .../translations/translations/zh-CN.json | 5 - 18 files changed, 462 insertions(+), 198 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts index a7d4e36d16f2ad..bff799798ff6e8 100644 --- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts @@ -3,11 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Datasource, NewDatasource, DatasourceInput } from '../types'; +import { Datasource, DatasourceInput } from '../types'; import { storedDatasourceToAgentDatasource } from './datasource_to_agent_datasource'; describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { - const mockNewDatasource: NewDatasource = { + const mockDatasource: Datasource = { + id: 'some-uuid', name: 'mock-datasource', description: '', config_id: '', @@ -15,11 +16,6 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { output_id: '', namespace: 'default', inputs: [], - }; - - const mockDatasource: Datasource = { - ...mockNewDatasource, - id: 'some-uuid', revision: 1, }; @@ -107,17 +103,6 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { }); }); - it('uses name for id when id is not provided in case of new datasource', () => { - expect(storedDatasourceToAgentDatasource(mockNewDatasource)).toEqual({ - id: 'mock-datasource', - name: 'mock-datasource', - namespace: 'default', - enabled: true, - use_output: 'default', - inputs: [], - }); - }); - it('returns agent datasource config with flattened input and package stream', () => { expect(storedDatasourceToAgentDatasource({ ...mockDatasource, inputs: [mockInput] })).toEqual({ id: 'some-uuid', diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts index 5deb33ccf10f1b..620b663451ea39 100644 --- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts +++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts @@ -3,16 +3,16 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Datasource, NewDatasource, FullAgentConfigDatasource } from '../types'; +import { Datasource, FullAgentConfigDatasource } from '../types'; import { DEFAULT_OUTPUT } from '../constants'; export const storedDatasourceToAgentDatasource = ( - datasource: Datasource | NewDatasource + datasource: Datasource ): FullAgentConfigDatasource => { - const { name, namespace, enabled, package: pkg, inputs } = datasource; + const { id, name, namespace, enabled, package: pkg, inputs } = datasource; const fullDatasource: FullAgentConfigDatasource = { - id: 'id' in datasource ? datasource.id : name, + id: id || name, name, namespace, enabled, diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts index 7705956590c161..96121251b133eb 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts @@ -3,8 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { SavedObjectAttributes } from 'src/core/public'; import { Datasource, DatasourcePackage, @@ -26,7 +24,7 @@ export interface NewAgentConfig { monitoring_enabled?: Array<'logs' | 'metrics'>; } -export interface AgentConfig extends NewAgentConfig, SavedObjectAttributes { +export interface AgentConfig extends NewAgentConfig { id: string; status: AgentConfigStatus; datasources: string[] | Datasource[]; diff --git a/x-pack/plugins/ingest_manager/common/types/models/datasource.ts b/x-pack/plugins/ingest_manager/common/types/models/datasource.ts index 885e0a9316d793..ca61a93d9be93e 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/datasource.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/datasource.ts @@ -17,22 +17,29 @@ export interface DatasourceConfigRecordEntry { export type DatasourceConfigRecord = Record; -export interface DatasourceInputStream { +export interface NewDatasourceInputStream { id: string; enabled: boolean; dataset: string; processors?: string[]; config?: DatasourceConfigRecord; vars?: DatasourceConfigRecord; +} + +export interface DatasourceInputStream extends NewDatasourceInputStream { agent_stream?: any; } -export interface DatasourceInput { +export interface NewDatasourceInput { type: string; enabled: boolean; processors?: string[]; config?: DatasourceConfigRecord; vars?: DatasourceConfigRecord; + streams: NewDatasourceInputStream[]; +} + +export interface DatasourceInput extends Omit { streams: DatasourceInputStream[]; } @@ -44,10 +51,11 @@ export interface NewDatasource { enabled: boolean; package?: DatasourcePackage; output_id: string; - inputs: DatasourceInput[]; + inputs: NewDatasourceInput[]; } -export type Datasource = NewDatasource & { +export interface Datasource extends Omit { id: string; + inputs: DatasourceInput[]; revision: number; -}; +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts index 0d19ecd0cb7351..e2fc190e158f97 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts @@ -5,12 +5,18 @@ */ import { sendRequest, useRequest } from './use_request'; import { datasourceRouteService } from '../../services'; -import { CreateDatasourceRequest, CreateDatasourceResponse } from '../../types'; +import { + CreateDatasourceRequest, + CreateDatasourceResponse, + UpdateDatasourceRequest, + UpdateDatasourceResponse, +} from '../../types'; import { DeleteDatasourcesRequest, DeleteDatasourcesResponse, GetDatasourcesRequest, GetDatasourcesResponse, + GetOneDatasourceResponse, } from '../../../../../common/types/rest_spec'; export const sendCreateDatasource = (body: CreateDatasourceRequest['body']) => { @@ -21,6 +27,17 @@ export const sendCreateDatasource = (body: CreateDatasourceRequest['body']) => { }); }; +export const sendUpdateDatasource = ( + datasourceId: string, + body: UpdateDatasourceRequest['body'] +) => { + return sendRequest({ + path: datasourceRouteService.getUpdatePath(datasourceId), + method: 'put', + body: JSON.stringify(body), + }); +}; + export const sendDeleteDatasource = (body: DeleteDatasourcesRequest['body']) => { return sendRequest({ path: datasourceRouteService.getDeletePath(), @@ -36,3 +53,10 @@ export function useGetDatasources(query: GetDatasourcesRequest['query']) { query, }); } + +export const sendGetOneDatasource = (datasourceId: string) => { + return sendRequest({ + path: datasourceRouteService.getInfoPath(datasourceId), + method: 'get', + }); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx index 39d882f7fdf65e..f1e3fea6a07423 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx @@ -39,17 +39,29 @@ export const CreateDatasourcePageLayout: React.FunctionComponent<{

- + {from === 'edit' ? ( + + ) : ( + + )}

- {from === 'config' ? ( + {from === 'edit' ? ( + + ) : from === 'config' ? ( - {agentConfig && from === 'config' ? ( + {agentConfig && (from === 'config' || from === 'edit') ? ( { const updatePackageInfo = (updatedPackageInfo: PackageInfo | undefined) => { if (updatedPackageInfo) { setPackageInfo(updatedPackageInfo); + setFormState('VALID'); } else { setFormState('INVALID'); setPackageInfo(undefined); @@ -152,9 +153,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { const cancelUrl = from === 'config' ? CONFIG_URL : PACKAGE_URL; // Save datasource - const [formState, setFormState] = useState< - 'VALID' | 'INVALID' | 'CONFIRM' | 'LOADING' | 'SUBMITTED' - >('INVALID'); + const [formState, setFormState] = useState('INVALID'); const saveDatasource = async () => { setFormState('LOADING'); const result = await sendCreateDatasource(datasource); @@ -174,6 +173,23 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { const { error } = await saveDatasource(); if (!error) { history.push(`${AGENT_CONFIG_DETAILS_PATH}${agentConfig ? agentConfig.id : configId}`); + notifications.toasts.addSuccess({ + title: i18n.translate('xpack.ingestManager.createDatasource.addedNotificationTitle', { + defaultMessage: `Successfully added '{datasourceName}'`, + values: { + datasourceName: datasource.name, + }, + }), + text: + agentCount && agentConfig + ? i18n.translate('xpack.ingestManager.createDatasource.addedNotificationMessage', { + defaultMessage: `Fleet will deploy updates to all agents that use the '{agentConfigName}' configuration`, + values: { + agentConfigName: agentConfig.name, + }, + }) + : undefined, + }); } else { notifications.toasts.addError(error, { title: 'Error', @@ -229,6 +245,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { packageInfo={packageInfo} datasource={datasource} updateDatasource={updateDatasource} + validationResults={validationResults!} /> ) : null, }, @@ -240,7 +257,6 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { children: agentConfig && packageInfo ? ( ) => void; validationResults: DatasourceValidationResults; submitAttempted: boolean; -}> = ({ - agentConfig, - packageInfo, - datasource, - updateDatasource, - validationResults, - submitAttempted, -}) => { - // Form show/hide states - +}> = ({ packageInfo, datasource, updateDatasource, validationResults, submitAttempted }) => { const hasErrors = validationResults ? validationHasErrors(validationResults) : false; - // Update datasource's package and config info - useEffect(() => { - const dsPackage = datasource.package; - const currentPkgKey = dsPackage ? `${dsPackage.name}-${dsPackage.version}` : ''; - const pkgKey = `${packageInfo.name}-${packageInfo.version}`; - - // If package has changed, create shell datasource with input&stream values based on package info - if (currentPkgKey !== pkgKey) { - // Existing datasources on the agent config using the package name, retrieve highest number appended to datasource name - const dsPackageNamePattern = new RegExp(`${packageInfo.name}-(\\d+)`); - const dsWithMatchingNames = (agentConfig.datasources as Datasource[]) - .filter(ds => Boolean(ds.name.match(dsPackageNamePattern))) - .map(ds => parseInt(ds.name.match(dsPackageNamePattern)![1], 10)) - .sort(); - - updateDatasource({ - name: `${packageInfo.name}-${ - dsWithMatchingNames.length ? dsWithMatchingNames[dsWithMatchingNames.length - 1] + 1 : 1 - }`, - package: { - name: packageInfo.name, - title: packageInfo.title, - version: packageInfo.version, - }, - inputs: packageToConfigDatasourceInputs(packageInfo), - }); - } - - // If agent config has changed, update datasource's config ID and namespace - if (datasource.config_id !== agentConfig.id) { - updateDatasource({ - config_id: agentConfig.id, - namespace: agentConfig.namespace, - }); - } - }, [datasource.package, datasource.config_id, agentConfig, packageInfo, updateDatasource]); - - // Step B, configure inputs (and their streams) + // Configure inputs (and their streams) // Assume packages only export one datasource for now const renderConfigureInputs = () => packageInfo.datasources && diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx index 792389381eaf04..c4d602c2c20819 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx @@ -17,13 +17,16 @@ import { } from '@elastic/eui'; import { AgentConfig, PackageInfo, Datasource, NewDatasource } from '../../../types'; import { packageToConfigDatasourceInputs } from '../../../services'; +import { Loading } from '../../../components'; +import { DatasourceValidationResults } from './services'; export const StepDefineDatasource: React.FunctionComponent<{ agentConfig: AgentConfig; packageInfo: PackageInfo; datasource: NewDatasource; updateDatasource: (fields: Partial) => void; -}> = ({ agentConfig, packageInfo, datasource, updateDatasource }) => { + validationResults: DatasourceValidationResults; +}> = ({ agentConfig, packageInfo, datasource, updateDatasource, validationResults }) => { // Form show/hide states const [isShowingAdvancedDefine, setIsShowingAdvancedDefine] = useState(false); @@ -64,11 +67,13 @@ export const StepDefineDatasource: React.FunctionComponent<{ } }, [datasource.package, datasource.config_id, agentConfig, packageInfo, updateDatasource]); - return ( + return validationResults ? ( <> } + isInvalid={!!validationResults.description} + error={validationResults.description} > ) : null} + ) : ( + ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts index 85cc758fc4c464..10b30a5696d834 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export type CreateDatasourceFrom = 'package' | 'config'; +export type CreateDatasourceFrom = 'package' | 'config' | 'edit'; +export type DatasourceFormState = 'VALID' | 'INVALID' | 'CONFIRM' | 'LOADING' | 'SUBMITTED'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx index 1eee9f6b0c3462..a0418c5f256c48 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx @@ -19,7 +19,7 @@ import { import { AgentConfig, Datasource } from '../../../../../types'; import { TableRowActions } from '../../../components/table_row_actions'; import { DangerEuiContextMenuItem } from '../../../components/danger_eui_context_menu_item'; -import { useCapabilities } from '../../../../../hooks'; +import { useCapabilities, useLink } from '../../../../../hooks'; import { useAgentConfigLink } from '../../hooks/use_details_uri'; import { DatasourceDeleteProvider } from '../../../components/datasource_delete_provider'; import { useConfigRefresh } from '../../hooks/use_config'; @@ -56,6 +56,7 @@ export const DatasourcesTable: React.FunctionComponent = ({ }) => { const hasWriteCapabilities = useCapabilities().write; const addDatasourceLink = useAgentConfigLink('add-datasource', { configId: config.id }); + const editDatasourceLink = useLink(`/configs/${config.id}/edit-datasource`); const refreshConfig = useConfigRefresh(); // With the datasources provided on input, generate the list of datasources @@ -201,22 +202,21 @@ export const DatasourcesTable: React.FunctionComponent = ({ {}} + // key="datasourceView" + // > + // + // , {}} - key="datasourceView" - > - - , - // FIXME: implement Edit datasource action - {}} + href={`${editDatasourceLink}/${datasource.id}`} key="datasourceEdit" > = ({ /> , // FIXME: implement Copy datasource action - {}} key="datasourceCopy"> - - , + // {}} key="datasourceCopy"> + // + // , {deleteDatasourcePrompt => { return ( @@ -256,7 +256,7 @@ export const DatasourcesTable: React.FunctionComponent = ({ ], }, ], - [config, hasWriteCapabilities, refreshConfig] + [config, editDatasourceLink, hasWriteCapabilities, refreshConfig] ); return ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx new file mode 100644 index 00000000000000..d4c39f21a1ea6c --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx @@ -0,0 +1,323 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useState, useEffect } from 'react'; +import { useRouteMatch, useHistory } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiButtonEmpty, + EuiButton, + EuiSteps, + EuiBottomBar, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; +import { AGENT_CONFIG_DETAILS_PATH } from '../../../constants'; +import { AgentConfig, PackageInfo, NewDatasource } from '../../../types'; +import { + useLink, + useCore, + useConfig, + sendUpdateDatasource, + sendGetAgentStatus, + sendGetOneAgentConfig, + sendGetOneDatasource, + sendGetPackageInfoByKey, +} from '../../../hooks'; +import { Loading, Error } from '../../../components'; +import { + CreateDatasourcePageLayout, + ConfirmCreateDatasourceModal, +} from '../create_datasource_page/components'; +import { + DatasourceValidationResults, + validateDatasource, + validationHasErrors, +} from '../create_datasource_page/services'; +import { DatasourceFormState, CreateDatasourceFrom } from '../create_datasource_page/types'; +import { StepConfigureDatasource } from '../create_datasource_page/step_configure_datasource'; +import { StepDefineDatasource } from '../create_datasource_page/step_define_datasource'; + +export const EditDatasourcePage: React.FunctionComponent = () => { + const { notifications } = useCore(); + const { + fleet: { enabled: isFleetEnabled }, + } = useConfig(); + const { + params: { configId, datasourceId }, + } = useRouteMatch(); + const history = useHistory(); + + // Agent config, package info, and datasource states + const [isLoadingData, setIsLoadingData] = useState(true); + const [loadingError, setLoadingError] = useState(); + const [agentConfig, setAgentConfig] = useState(); + const [packageInfo, setPackageInfo] = useState(); + const [datasource, setDatasource] = useState({ + name: '', + description: '', + config_id: '', + enabled: true, + output_id: '', + inputs: [], + }); + + // Retrieve agent config, package, and datasource info + useEffect(() => { + const getData = async () => { + setIsLoadingData(true); + setLoadingError(undefined); + try { + const [{ data: agentConfigData }, { data: datasourceData }] = await Promise.all([ + sendGetOneAgentConfig(configId), + sendGetOneDatasource(datasourceId), + ]); + if (agentConfigData?.item) { + setAgentConfig(agentConfigData.item); + } + if (datasourceData?.item) { + const { id, revision, inputs, ...restOfDatasource } = datasourceData.item; + // Remove `agent_stream` from all stream info, we assign this after saving + const newDatasource = { + ...restOfDatasource, + inputs: inputs.map(input => { + const { streams, ...restOfInput } = input; + return { + ...restOfInput, + streams: streams.map(stream => { + const { agent_stream, ...restOfStream } = stream; + return restOfStream; + }), + }; + }), + }; + setDatasource(newDatasource); + if (datasourceData.item.package) { + const { data: packageData } = await sendGetPackageInfoByKey( + `${datasourceData.item.package.name}-${datasourceData.item.package.version}` + ); + if (packageData?.response) { + setPackageInfo(packageData.response); + setValidationResults(validateDatasource(newDatasource, packageData.response)); + setFormState('VALID'); + } + } + } + } catch (e) { + setLoadingError(e); + } + setIsLoadingData(false); + }; + getData(); + }, [configId, datasourceId]); + + // Retrieve agent count + const [agentCount, setAgentCount] = useState(0); + useEffect(() => { + const getAgentCount = async () => { + const { data } = await sendGetAgentStatus({ configId }); + if (data?.results.total) { + setAgentCount(data.results.total); + } + }; + + if (isFleetEnabled) { + getAgentCount(); + } + }, [configId, isFleetEnabled]); + + // Datasource validation state + const [validationResults, setValidationResults] = useState(); + const hasErrors = validationResults ? validationHasErrors(validationResults) : false; + + // Update datasource method + const updateDatasource = (updatedFields: Partial) => { + const newDatasource = { + ...datasource, + ...updatedFields, + }; + setDatasource(newDatasource); + + // eslint-disable-next-line no-console + console.debug('Datasource updated', newDatasource); + const newValidationResults = updateDatasourceValidation(newDatasource); + const hasValidationErrors = newValidationResults + ? validationHasErrors(newValidationResults) + : false; + if (!hasValidationErrors) { + setFormState('VALID'); + } + }; + + const updateDatasourceValidation = (newDatasource?: NewDatasource) => { + if (packageInfo) { + const newValidationResult = validateDatasource(newDatasource || datasource, packageInfo); + setValidationResults(newValidationResult); + // eslint-disable-next-line no-console + console.debug('Datasource validation results', newValidationResult); + + return newValidationResult; + } + }; + + // Cancel url + const CONFIG_URL = useLink(`${AGENT_CONFIG_DETAILS_PATH}${configId}`); + const cancelUrl = CONFIG_URL; + + // Save datasource + const [formState, setFormState] = useState('INVALID'); + const saveDatasource = async () => { + setFormState('LOADING'); + const result = await sendUpdateDatasource(datasourceId, datasource); + setFormState('SUBMITTED'); + return result; + }; + + const onSubmit = async () => { + if (formState === 'VALID' && hasErrors) { + setFormState('INVALID'); + return; + } + if (agentCount !== 0 && formState !== 'CONFIRM') { + setFormState('CONFIRM'); + return; + } + const { error } = await saveDatasource(); + if (!error) { + history.push(`${AGENT_CONFIG_DETAILS_PATH}${configId}`); + notifications.toasts.addSuccess({ + title: i18n.translate('xpack.ingestManager.editDatasource.updatedNotificationTitle', { + defaultMessage: `Successfully updated '{datasourceName}'`, + values: { + datasourceName: datasource.name, + }, + }), + text: + agentCount && agentConfig + ? i18n.translate('xpack.ingestManager.editDatasource.updatedNotificationMessage', { + defaultMessage: `Fleet will deploy updates to all agents that use the '{agentConfigName}' configuration`, + values: { + agentConfigName: agentConfig.name, + }, + }) + : undefined, + }); + } else { + notifications.toasts.addError(error, { + title: 'Error', + }); + setFormState('VALID'); + } + }; + + const layoutProps = { + from: 'edit' as CreateDatasourceFrom, + cancelUrl, + agentConfig, + packageInfo, + }; + + return ( + + {isLoadingData ? ( + + ) : loadingError || !agentConfig || !packageInfo ? ( + + } + error={ + loadingError || + i18n.translate('xpack.ingestManager.editDatasource.errorLoadingDataMessage', { + defaultMessage: 'There was an error loading this data source information', + }) + } + /> + ) : ( + <> + {formState === 'CONFIRM' && ( + setFormState('VALID')} + /> + )} + + ), + }, + { + title: i18n.translate( + 'xpack.ingestManager.editDatasource.stepConfgiureDatasourceTitle', + { + defaultMessage: 'Select the data you want to collect', + } + ), + children: ( + + ), + }, + ]} + /> + + + + + + + + + + + + + + + + + )} + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx index 71ada155373bfb..ef88aa5d17f1e4 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx @@ -8,10 +8,14 @@ import { HashRouter as Router, Switch, Route } from 'react-router-dom'; import { AgentConfigListPage } from './list_page'; import { AgentConfigDetailsPage } from './details_page'; import { CreateDatasourcePage } from './create_datasource_page'; +import { EditDatasourcePage } from './edit_datasource_page'; export const AgentConfigApp: React.FunctionComponent = () => ( + + + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx index 1ea162252c741e..3dcc19bc4a5aee 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx @@ -36,13 +36,11 @@ import { useConfig, useUrlParams, } from '../../../hooks'; -import { AgentConfigDeleteProvider } from '../components'; import { CreateAgentConfigFlyout } from './components'; import { SearchBar } from '../../../components/search_bar'; import { LinkedAgentCount } from '../components'; import { useAgentConfigLink } from '../details_page/hooks/use_details_uri'; import { TableRowActions } from '../components/table_row_actions'; -import { DangerEuiContextMenuItem } from '../components/danger_eui_context_menu_item'; const NO_WRAP_TRUNCATE_STYLE: CSSProperties = Object.freeze({ overflow: 'hidden', @@ -108,30 +106,12 @@ const ConfigRowActions = memo<{ config: AgentConfig; onDelete: () => void }>( defaultMessage="Create data source" /> , - - - - , - - - {deleteAgentConfigsPrompt => { - return ( - deleteAgentConfigsPrompt([config.id], onDelete)} - > - - - ); - }} - , + // + // + // , ]} /> ); @@ -156,7 +136,6 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { : urlParams.kuery ?? '' ); const { pagination, pageSizeOptions, setPagination } = usePagination(); - const [selectedAgentConfigs, setSelectedAgentConfigs] = useState([]); const history = useHistory(); const isCreateAgentConfigFlyoutOpen = 'create' in urlParams; const setIsCreateAgentConfigFlyoutOpen = useCallback( @@ -321,34 +300,6 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { /> ) : null} - {selectedAgentConfigs.length ? ( - - - {deleteAgentConfigsPrompt => ( - { - deleteAgentConfigsPrompt( - selectedAgentConfigs.map(agentConfig => agentConfig.id), - () => { - sendRequest(); - setSelectedAgentConfigs([]); - } - ); - }} - > - - - )} - - - ) : null} = () => { items={agentConfigData ? agentConfigData.items : []} itemId="id" columns={columns} - isSelectable={true} - selection={{ - selectable: (agentConfig: AgentConfig) => !agentConfig.is_default, - onSelectionChange: (newSelectedAgentConfigs: AgentConfig[]) => { - setSelectedAgentConfigs(newSelectedAgentConfigs); - }, - }} + isSelectable={false} pagination={{ pageIndex: pagination.currentPage - 1, pageSize: pagination.pageSize, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts index 2f78ecd1b085ee..1508f4dfaa628b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts @@ -32,6 +32,8 @@ export { // API schemas - Datasource CreateDatasourceRequest, CreateDatasourceResponse, + UpdateDatasourceRequest, + UpdateDatasourceResponse, // API schemas - Data Streams GetDataStreamsResponse, // API schemas - Agents diff --git a/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts index 42298960cc615d..69f14854cdd0fc 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts @@ -22,6 +22,7 @@ import { } from '../../types'; import { GetAgentConfigsResponse, + GetAgentConfigsResponseItem, GetOneAgentConfigResponse, CreateAgentConfigResponse, UpdateAgentConfigResponse, @@ -46,7 +47,7 @@ export const getAgentConfigsHandler: RequestHandler< await bluebird.map( items, - agentConfig => + (agentConfig: GetAgentConfigsResponseItem) => listAgents(soClient, { showInactive: true, perPage: 0, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8974f0b5b4d582..81dc44f3a4cb41 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8225,11 +8225,8 @@ "xpack.ingestManager.agentConfigList.addButton": "エージェント構成を作成", "xpack.ingestManager.agentConfigList.agentsColumnTitle": "エージェント", "xpack.ingestManager.agentConfigList.clearFiltersLinkText": "フィルターを消去", - "xpack.ingestManager.agentConfigList.copyConfigActionText": "構成をコピー", "xpack.ingestManager.agentConfigList.createDatasourceActionText": "データソースを作成", "xpack.ingestManager.agentConfigList.datasourcesCountColumnTitle": "データソース", - "xpack.ingestManager.agentConfigList.deleteButton": "{count, plural, one {# エージェント設定} other {# エージェント設定}}を削除", - "xpack.ingestManager.agentConfigList.deleteConfigActionText": "構成の削除", "xpack.ingestManager.agentConfigList.descriptionColumnTitle": "説明", "xpack.ingestManager.agentConfigList.loadingAgentConfigsMessage": "エージェント構成を読み込み中...", "xpack.ingestManager.agentConfigList.nameColumnTitle": "名前", @@ -8313,7 +8310,6 @@ "xpack.ingestManager.configDetails.configDetailsTitle": "構成「{id}」", "xpack.ingestManager.configDetails.configNotFoundErrorTitle": "構成「{id}」が見つかりません", "xpack.ingestManager.configDetails.datasourcesTable.actionsColumnTitle": "アクション", - "xpack.ingestManager.configDetails.datasourcesTable.copyActionTitle": "データソースをコピー", "xpack.ingestManager.configDetails.datasourcesTable.deleteActionTitle": "データソースを削除", "xpack.ingestManager.configDetails.datasourcesTable.descriptionColumnTitle": "説明", "xpack.ingestManager.configDetails.datasourcesTable.editActionTitle": "データソースを編集", @@ -8321,7 +8317,6 @@ "xpack.ingestManager.configDetails.datasourcesTable.namespaceColumnTitle": "名前空間", "xpack.ingestManager.configDetails.datasourcesTable.packageNameColumnTitle": "パッケージ", "xpack.ingestManager.configDetails.datasourcesTable.streamsCountColumnTitle": "ストリーム", - "xpack.ingestManager.configDetails.datasourcesTable.viewActionTitle": "データソースを表示", "xpack.ingestManager.configDetails.subTabs.datasouces": "データソース", "xpack.ingestManager.configDetails.subTabs.settings": "設定", "xpack.ingestManager.configDetails.subTabs.yamlFile": "YAML ファイル", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d36a62f15aee9e..e06edb45de8fa9 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8228,11 +8228,8 @@ "xpack.ingestManager.agentConfigList.addButton": "创建代理配置", "xpack.ingestManager.agentConfigList.agentsColumnTitle": "代理", "xpack.ingestManager.agentConfigList.clearFiltersLinkText": "清除筛选", - "xpack.ingestManager.agentConfigList.copyConfigActionText": "复制配置", "xpack.ingestManager.agentConfigList.createDatasourceActionText": "创建数据源", "xpack.ingestManager.agentConfigList.datasourcesCountColumnTitle": "数据源", - "xpack.ingestManager.agentConfigList.deleteButton": "删除 {count, plural, one {# 个代理配置} other {# 个代理配置}}", - "xpack.ingestManager.agentConfigList.deleteConfigActionText": "删除配置", "xpack.ingestManager.agentConfigList.descriptionColumnTitle": "描述", "xpack.ingestManager.agentConfigList.loadingAgentConfigsMessage": "正在加载代理配置……", "xpack.ingestManager.agentConfigList.nameColumnTitle": "名称", @@ -8316,7 +8313,6 @@ "xpack.ingestManager.configDetails.configDetailsTitle": "配置“{id}”", "xpack.ingestManager.configDetails.configNotFoundErrorTitle": "未找到配置“{id}”", "xpack.ingestManager.configDetails.datasourcesTable.actionsColumnTitle": "操作", - "xpack.ingestManager.configDetails.datasourcesTable.copyActionTitle": "复制数据源", "xpack.ingestManager.configDetails.datasourcesTable.deleteActionTitle": "删除数据源", "xpack.ingestManager.configDetails.datasourcesTable.descriptionColumnTitle": "描述", "xpack.ingestManager.configDetails.datasourcesTable.editActionTitle": "编辑数据源", @@ -8324,7 +8320,6 @@ "xpack.ingestManager.configDetails.datasourcesTable.namespaceColumnTitle": "命名空间", "xpack.ingestManager.configDetails.datasourcesTable.packageNameColumnTitle": "软件包", "xpack.ingestManager.configDetails.datasourcesTable.streamsCountColumnTitle": "流计数", - "xpack.ingestManager.configDetails.datasourcesTable.viewActionTitle": "查看数据源", "xpack.ingestManager.configDetails.subTabs.datasouces": "数据源", "xpack.ingestManager.configDetails.subTabs.settings": "设置", "xpack.ingestManager.configDetails.subTabs.yamlFile": "YAML 文件", From bcda1096e170b82c5bff02360b68b3255a46efb6 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Wed, 29 Apr 2020 19:58:27 -0600 Subject: [PATCH 17/30] [SIEM][Lists] Removes plugin dependencies, adds more unit tests, fixes more TypeScript types * Removes plugin dependencies for better integration outside of Requests such as alerting * Adds more unit tests * Fixes more TypeScript types to be more normalized * Makes this work with the user 'elastic' if security is turned off - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../plugins/lists/server/get_space_id.test.ts | 46 ++++ .../utils/get_space.ts => get_space_id.ts} | 4 +- x-pack/plugins/lists/server/get_user.test.ts | 67 ++++++ .../server/{services/utils => }/get_user.ts | 16 +- x-pack/plugins/lists/server/plugin.ts | 24 +-- .../services/items/buffer_lines.test.ts | 2 +- .../services/items/create_list_item.test.ts | 4 +- .../server/services/items/create_list_item.ts | 8 +- .../items/create_list_items_bulk.test.ts | 6 +- .../services/items/create_list_items_bulk.ts | 8 +- .../services/items/delete_list_item.test.ts | 11 +- .../server/services/items/delete_list_item.ts | 11 +- .../items/delete_list_item_by_value.test.ts | 2 +- .../items/delete_list_item_by_value.ts | 11 +- .../services/items/get_list_item.test.ts | 17 +- .../server/services/items/get_list_item.ts | 27 ++- .../services/items/get_list_item_by_value.ts | 9 +- .../items/get_list_item_by_values.test.ts | 21 +- .../services/items/get_list_item_by_values.ts | 29 ++- .../items/get_list_item_index.test.ts | 15 +- .../services/items/get_list_item_index.ts | 15 +- .../server/services/items/update_list_item.ts | 10 +- .../items/write_lines_to_bulk_list_items.ts | 17 +- .../items/write_list_items_to_stream.test.ts | 27 ++- .../items/write_list_items_to_stream.ts | 22 +- .../lists/server/services/lists/client.ts | 202 +++++++----------- .../server/services/lists/client_types.ts | 11 +- .../server/services/lists/create_list.test.ts | 4 +- .../server/services/lists/create_list.ts | 8 +- .../server/services/lists/delete_list.test.ts | 6 +- .../server/services/lists/delete_list.ts | 13 +- .../server/services/lists/get_list.test.ts | 10 +- .../lists/server/services/lists/get_list.ts | 8 +- .../services/lists/get_list_index.test.ts | 15 +- .../server/services/lists/get_list_index.ts | 12 +- .../server/services/lists/update_list.ts | 10 +- ...lient_mock.ts => get_call_cluster_mock.ts} | 17 +- .../get_create_list_item_bulk_options_mock.ts | 4 +- .../get_create_list_item_options_mock.ts | 4 +- .../mocks/get_create_list_options_mock.ts | 4 +- ..._delete_list_item_by_value_options_mock.ts | 4 +- .../get_delete_list_item_options_mock.ts | 4 +- .../mocks/get_delete_list_options_mock.ts | 4 +- ...mport_list_items_to_stream_options_mock.ts | 4 +- .../get_list_item_by_value_options_mock.ts | 4 +- .../get_list_item_by_values_options_mock.ts | 4 +- .../get_update_list_item_options_mock.ts | 4 +- .../mocks/get_update_list_options_mock.ts | 4 +- .../get_write_buffer_to_items_options_mock.ts | 4 +- ...write_list_items_to_stream_options_mock.ts | 8 +- .../lists/server/services/mocks/index.ts | 4 +- .../utils/derive_type_from_es_type.test.ts | 8 + .../get_query_filter_from_type_value.test.ts | 92 ++++++++ .../lists/server/services/utils/index.ts | 2 - .../transform_elastic_to_list_item.test.ts | 69 ++++++ .../utils/transform_elastic_to_list_item.ts | 13 +- ...ansform_list_item_to_elastic_query.test.ts | 47 ++++ .../transform_list_item_to_elastic_query.ts | 7 +- x-pack/plugins/lists/server/types.ts | 5 +- 59 files changed, 650 insertions(+), 398 deletions(-) create mode 100644 x-pack/plugins/lists/server/get_space_id.test.ts rename x-pack/plugins/lists/server/{services/utils/get_space.ts => get_space_id.ts} (83%) create mode 100644 x-pack/plugins/lists/server/get_user.test.ts rename x-pack/plugins/lists/server/{services/utils => }/get_user.ts (54%) rename x-pack/plugins/lists/server/services/mocks/{get_data_client_mock.ts => get_call_cluster_mock.ts} (57%) create mode 100644 x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.test.ts create mode 100644 x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts create mode 100644 x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.test.ts diff --git a/x-pack/plugins/lists/server/get_space_id.test.ts b/x-pack/plugins/lists/server/get_space_id.test.ts new file mode 100644 index 00000000000000..9c1d11b71984d1 --- /dev/null +++ b/x-pack/plugins/lists/server/get_space_id.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { httpServerMock } from 'src/core/server/mocks'; +import { KibanaRequest } from 'src/core/server'; + +import { spacesServiceMock } from '../../spaces/server/spaces_service/spaces_service.mock'; + +import { getSpaceId } from './get_space_id'; + +describe('get_space_id', () => { + let request = KibanaRequest.from(httpServerMock.createRawRequest({})); + beforeEach(() => { + request = KibanaRequest.from(httpServerMock.createRawRequest({})); + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns "default" as the space id given a space id of "default"', () => { + const spaces = spacesServiceMock.createSetupContract(); + const space = getSpaceId({ request, spaces }); + expect(space).toEqual('default'); + }); + + test('it returns "another-space" as the space id given a space id of "another-space"', () => { + const spaces = spacesServiceMock.createSetupContract('another-space'); + const space = getSpaceId({ request, spaces }); + expect(space).toEqual('another-space'); + }); + + test('it returns "default" as the space id given a space id of undefined', () => { + const space = getSpaceId({ request, spaces: undefined }); + expect(space).toEqual('default'); + }); + + test('it returns "default" as the space id given a space id of null', () => { + const space = getSpaceId({ request, spaces: null }); + expect(space).toEqual('default'); + }); +}); diff --git a/x-pack/plugins/lists/server/services/utils/get_space.ts b/x-pack/plugins/lists/server/get_space_id.ts similarity index 83% rename from x-pack/plugins/lists/server/services/utils/get_space.ts rename to x-pack/plugins/lists/server/get_space_id.ts index e23f963b2c40d2..f224e37e044672 100644 --- a/x-pack/plugins/lists/server/services/utils/get_space.ts +++ b/x-pack/plugins/lists/server/get_space_id.ts @@ -6,9 +6,9 @@ import { KibanaRequest } from 'kibana/server'; -import { SpacesServiceSetup } from '../../../../spaces/server'; +import { SpacesServiceSetup } from '../../spaces/server'; -export const getSpace = ({ +export const getSpaceId = ({ spaces, request, }: { diff --git a/x-pack/plugins/lists/server/get_user.test.ts b/x-pack/plugins/lists/server/get_user.test.ts new file mode 100644 index 00000000000000..0992e3c361fcfe --- /dev/null +++ b/x-pack/plugins/lists/server/get_user.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { httpServerMock } from 'src/core/server/mocks'; +import { KibanaRequest } from 'src/core/server'; + +import { securityMock } from '../../security/server/mocks'; +import { SecurityPluginSetup } from '../../security/server'; + +import { getUser } from './get_user'; + +describe('get_user', () => { + let request = KibanaRequest.from(httpServerMock.createRawRequest({})); + beforeEach(() => { + jest.clearAllMocks(); + request = KibanaRequest.from(httpServerMock.createRawRequest({})); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns "bob" as the user given a security request with "bob"', () => { + const security: SecurityPluginSetup = securityMock.createSetup(); + security.authc.getCurrentUser = jest.fn().mockReturnValue({ username: 'bob' }); + const user = getUser({ request, security }); + expect(user).toEqual('bob'); + }); + + test('it returns "alice" as the user given a security request with "alice"', () => { + const security: SecurityPluginSetup = securityMock.createSetup(); + security.authc.getCurrentUser = jest.fn().mockReturnValue({ username: 'alice' }); + const user = getUser({ request, security }); + expect(user).toEqual('alice'); + }); + + test('it returns "elastic" as the user given null as the current user', () => { + const security: SecurityPluginSetup = securityMock.createSetup(); + security.authc.getCurrentUser = jest.fn().mockReturnValue(null); + const user = getUser({ request, security }); + expect(user).toEqual('elastic'); + }); + + test('it returns "elastic" as the user given undefined as the current user', () => { + const security: SecurityPluginSetup = securityMock.createSetup(); + security.authc.getCurrentUser = jest.fn().mockReturnValue(undefined); + const user = getUser({ request, security }); + expect(user).toEqual('elastic'); + }); + + test('it returns "elastic" as the user given undefined as the plugin', () => { + const security: SecurityPluginSetup = securityMock.createSetup(); + security.authc.getCurrentUser = jest.fn().mockReturnValue(undefined); + const user = getUser({ request, security: undefined }); + expect(user).toEqual('elastic'); + }); + + test('it returns "elastic" as the user given null as the plugin', () => { + const security: SecurityPluginSetup = securityMock.createSetup(); + security.authc.getCurrentUser = jest.fn().mockReturnValue(undefined); + const user = getUser({ request, security: null }); + expect(user).toEqual('elastic'); + }); +}); diff --git a/x-pack/plugins/lists/server/services/utils/get_user.ts b/x-pack/plugins/lists/server/get_user.ts similarity index 54% rename from x-pack/plugins/lists/server/services/utils/get_user.ts rename to x-pack/plugins/lists/server/get_user.ts index 1ddad047da7229..3b59853d0ab62a 100644 --- a/x-pack/plugins/lists/server/services/utils/get_user.ts +++ b/x-pack/plugins/lists/server/get_user.ts @@ -6,17 +6,21 @@ import { KibanaRequest } from 'kibana/server'; -import { SecurityPluginSetup } from '../../../../security/server'; +import { SecurityPluginSetup } from '../../security/server'; -interface GetUserOptions { - security: SecurityPluginSetup; +export interface GetUserOptions { + security: SecurityPluginSetup | null | undefined; request: KibanaRequest; } export const getUser = ({ security, request }: GetUserOptions): string => { - const authenticatedUser = security.authc.getCurrentUser(request); - if (authenticatedUser != null) { - return authenticatedUser.username; + if (security != null) { + const authenticatedUser = security.authc.getCurrentUser(request); + if (authenticatedUser != null) { + return authenticatedUser.username; + } else { + return 'elastic'; + } } else { return 'elastic'; } diff --git a/x-pack/plugins/lists/server/plugin.ts b/x-pack/plugins/lists/server/plugin.ts index 4473d68d3c6465..2498c36967a536 100644 --- a/x-pack/plugins/lists/server/plugin.ts +++ b/x-pack/plugins/lists/server/plugin.ts @@ -5,7 +5,7 @@ */ import { first } from 'rxjs/operators'; -import { ElasticsearchServiceSetup, Logger, PluginInitializerContext } from 'kibana/server'; +import { Logger, PluginInitializerContext } from 'kibana/server'; import { CoreSetup } from 'src/core/server'; import { SecurityPluginSetup } from '../../security/server'; @@ -16,12 +16,13 @@ import { initRoutes } from './routes/init_routes'; import { ListClient } from './services/lists/client'; import { ContextProvider, ContextProviderReturn, PluginsSetup } from './types'; import { createConfig$ } from './create_config'; +import { getSpaceId } from './get_space_id'; +import { getUser } from './get_user'; export class ListPlugin { private readonly logger: Logger; private spaces: SpacesServiceSetup | undefined | null; private config: ConfigType | undefined | null; - private elasticsearch: ElasticsearchServiceSetup | undefined | null; private security: SecurityPluginSetup | undefined | null; constructor(private readonly initializerContext: PluginInitializerContext) { @@ -38,7 +39,6 @@ export class ListPlugin { ); this.spaces = plugins.spaces?.spacesService; this.config = config; - this.elasticsearch = core.elasticsearch; this.security = plugins.security; core.http.registerRouteHandlerContext('lists', this.createRouteHandlerContext()); @@ -56,28 +56,28 @@ export class ListPlugin { private createRouteHandlerContext = (): ContextProvider => { return async (context, request): ContextProviderReturn => { - const { spaces, config, security, elasticsearch } = this; + const { spaces, config, security } = this; const { core: { - elasticsearch: { dataClient }, + elasticsearch: { + dataClient: { callAsCurrentUser }, + }, }, } = context; if (config == null) { throw new TypeError('Configuration is required for this plugin to operate'); - } else if (elasticsearch == null) { - throw new TypeError('Elastic Search is required for this plugin to operate'); - } else if (security == null) { - // TODO: This might be null, test authentication being turned off. - throw new TypeError('Security plugin is required for this plugin to operate'); } else { + const spaceId = getSpaceId({ request, spaces }); + const user = getUser({ request, security }); return { getListClient: (): ListClient => new ListClient({ + callCluster: callAsCurrentUser, config, - dataClient, request, security, - spaces, + spaceId, + user, }), }; } diff --git a/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts b/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts index 946e1c240be31c..48deb3ee86820d 100644 --- a/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts +++ b/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TestReadable } from '../mocks/test_readable'; +import { TestReadable } from '../mocks'; import { BufferLines } from './buffer_lines'; diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.test.ts b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts index b2bca241c468cb..abbb2701499557 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts @@ -31,7 +31,7 @@ describe('crete_list_item', () => { expect(listItem).toEqual(expected); }); - test('It calls "callAsCurrentUser" with body, index, and listIndex', async () => { + test('It calls "callCluster" with body, index, and listIndex', async () => { const options = getCreateListItemOptionsMock(); await createListItem(options); const body = getIndexESListItemMock(); @@ -40,7 +40,7 @@ describe('crete_list_item', () => { id: LIST_ITEM_ID, index: LIST_ITEM_INDEX, }; - expect(options.dataClient.callAsCurrentUser).toBeCalledWith('index', expected); + expect(options.callCluster).toBeCalledWith('index', expected); }); test('It returns an auto-generated id if id is sent in undefined', async () => { diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.ts b/x-pack/plugins/lists/server/services/items/create_list_item.ts index da1e192bf2412d..83a118b7951923 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_item.ts @@ -6,6 +6,7 @@ import uuid from 'uuid'; import { CreateDocumentResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; import { IdOrUndefined, @@ -14,7 +15,6 @@ import { MetaOrUndefined, Type, } from '../../../common/schemas'; -import { DataClient } from '../../types'; import { transformListItemToElasticQuery } from '../utils'; export interface CreateListItemOptions { @@ -22,7 +22,7 @@ export interface CreateListItemOptions { listId: string; type: Type; value: string; - dataClient: DataClient; + callCluster: APICaller; listItemIndex: string; user: string; meta: MetaOrUndefined; @@ -35,7 +35,7 @@ export const createListItem = async ({ listId, type, value, - dataClient, + callCluster, listItemIndex, user, meta, @@ -58,7 +58,7 @@ export const createListItem = async ({ ...transformListItemToElasticQuery({ type, value }), }; - const response: CreateDocumentResponse = await dataClient.callAsCurrentUser('index', { + const response: CreateDocumentResponse = await callCluster('index', { body, id, index: listItemIndex, diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts index 9263b975b20e74..94cc57b53b4e24 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts @@ -24,13 +24,13 @@ describe('crete_list_item_bulk', () => { jest.clearAllMocks(); }); - test('It calls "callAsCurrentUser" with body, index, and the bulk items', async () => { + test('It calls "callCluster" with body, index, and the bulk items', async () => { const options = getCreateListItemBulkOptionsMock(); await createListItemsBulk(options); const firstRecord: IndexEsListItemSchema = getIndexESListItemMock(); const secondRecord: IndexEsListItemSchema = getIndexESListItemMock(VALUE_2); [firstRecord.tie_breaker_id, secondRecord.tie_breaker_id] = TIE_BREAKERS; - expect(options.dataClient.callAsCurrentUser).toBeCalledWith('bulk', { + expect(options.callCluster).toBeCalledWith('bulk', { body: [ { create: { _index: LIST_ITEM_INDEX } }, firstRecord, @@ -44,6 +44,6 @@ describe('crete_list_item_bulk', () => { test('It should not call the dataClient when the values are empty', async () => { const options = getCreateListItemBulkOptionsMock(); options.value = []; - expect(options.dataClient.callAsCurrentUser).not.toBeCalled(); + expect(options.callCluster).not.toBeCalled(); }); }); diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts index 7100a5f8eaabca..eac294c5f244a2 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts @@ -5,9 +5,9 @@ */ import uuid from 'uuid'; +import { APICaller } from 'kibana/server'; import { transformListItemToElasticQuery } from '../utils'; -import { DataClient } from '../../types'; import { CreateEsBulkTypeSchema, IndexEsListItemSchema, @@ -19,7 +19,7 @@ export interface CreateListItemsBulkOptions { listId: string; type: Type; value: string[]; - dataClient: DataClient; + callCluster: APICaller; listItemIndex: string; user: string; meta: MetaOrUndefined; @@ -31,7 +31,7 @@ export const createListItemsBulk = async ({ listId, type, value, - dataClient, + callCluster, listItemIndex, user, meta, @@ -63,7 +63,7 @@ export const createListItemsBulk = async ({ [] ); - await dataClient.callAsCurrentUser('bulk', { + await callCluster('bulk', { body, index: listItemIndex, }); diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts index 795c579462b691..00fcefb2c379fc 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts @@ -4,8 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LIST_ITEM_ID, LIST_ITEM_INDEX, getListItemResponseMock } from '../mocks'; -import { getDeleteListItemOptionsMock } from '../mocks/get_delete_list_item_options_mock'; +import { + LIST_ITEM_ID, + LIST_ITEM_INDEX, + getDeleteListItemOptionsMock, + getListItemResponseMock, +} from '../mocks'; import { getListItem } from './get_list_item'; import { deleteListItem } from './delete_list_item'; @@ -37,6 +41,7 @@ describe('delete_list_item', () => { const deletedListItem = await deleteListItem(options); expect(deletedListItem).toEqual(listItem); }); + test('Delete calls "delete" if a list item is returned from "getListItem"', async () => { const listItem = getListItemResponseMock(); ((getListItem as unknown) as jest.Mock).mockResolvedValueOnce(listItem); @@ -46,6 +51,6 @@ describe('delete_list_item', () => { id: LIST_ITEM_ID, index: LIST_ITEM_INDEX, }; - expect(options.dataClient.callAsCurrentUser).toBeCalledWith('delete', deleteQuery); + expect(options.callCluster).toBeCalledWith('delete', deleteQuery); }); }); diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.ts index ffce2d3b2af816..9992f43387c89f 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item.ts @@ -4,27 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ +import { APICaller } from 'kibana/server'; + import { Id, ListItemSchema } from '../../../common/schemas'; -import { DataClient } from '../../types'; import { getListItem } from '.'; export interface DeleteListItemOptions { id: Id; - dataClient: DataClient; + callCluster: APICaller; listItemIndex: string; } export const deleteListItem = async ({ id, - dataClient, + callCluster, listItemIndex, }: DeleteListItemOptions): Promise => { - const listItem = await getListItem({ dataClient, id, listItemIndex }); + const listItem = await getListItem({ callCluster, id, listItemIndex }); if (listItem == null) { return null; } else { - await dataClient.callAsCurrentUser('delete', { + await callCluster('delete', { id, index: listItemIndex, }); diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts index dee890445f9a3b..c7c80638e4c374 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts @@ -52,6 +52,6 @@ describe('delete_list_item_by_value', () => { }, index: '.items', }; - expect(options.dataClient.callAsCurrentUser).toBeCalledWith('deleteByQuery', deleteByQuery); + expect(options.callCluster).toBeCalledWith('deleteByQuery', deleteByQuery); }); }); diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts index f2f5ec3078e621..ec29f14a0ff647 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { APICaller } from 'kibana/server'; + import { ListItemArraySchema, Type } from '../../../common/schemas'; import { getQueryFilterFromTypeValue } from '../utils'; -import { DataClient } from '../../types'; import { getListItemByValues } from './get_list_item_by_values'; @@ -14,7 +15,7 @@ export interface DeleteListItemByValueOptions { listId: string; type: Type; value: string; - dataClient: DataClient; + callCluster: APICaller; listItemIndex: string; } @@ -22,11 +23,11 @@ export const deleteListItemByValue = async ({ listId, value, type, - dataClient, + callCluster, listItemIndex, }: DeleteListItemByValueOptions): Promise => { const listItems = await getListItemByValues({ - dataClient, + callCluster, listId, listItemIndex, type, @@ -38,7 +39,7 @@ export const deleteListItemByValue = async ({ type, value: values, }); - await dataClient.callAsCurrentUser('deleteByQuery', { + await callCluster('deleteByQuery', { body: { query: { bool: { diff --git a/x-pack/plugins/lists/server/services/items/get_list_item.test.ts b/x-pack/plugins/lists/server/services/items/get_list_item.test.ts index 937993f1d8f713..31a421c2e31bfe 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item.test.ts @@ -4,8 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LIST_ID, LIST_INDEX, getDataClientMock, getListItemResponseMock } from '../mocks'; -import { getSearchListItemMock } from '../mocks/get_search_list_item_mock'; +import { + LIST_ID, + LIST_INDEX, + getCallClusterMock, + getListItemResponseMock, + getSearchListItemMock, +} from '../mocks'; import { getListItem } from './get_list_item'; @@ -20,8 +25,8 @@ describe('get_list_item', () => { test('it returns a list item as expected if the list item is found', async () => { const data = getSearchListItemMock(); - const dataClient = getDataClientMock(data); - const list = await getListItem({ dataClient, id: LIST_ID, listItemIndex: LIST_INDEX }); + const callCluster = getCallClusterMock(data); + const list = await getListItem({ callCluster, id: LIST_ID, listItemIndex: LIST_INDEX }); const expected = getListItemResponseMock(); expect(list).toEqual(expected); }); @@ -29,8 +34,8 @@ describe('get_list_item', () => { test('it returns null if the search is empty', async () => { const data = getSearchListItemMock(); data.hits.hits = []; - const dataClient = getDataClientMock(data); - const list = await getListItem({ dataClient, id: LIST_ID, listItemIndex: LIST_INDEX }); + const callCluster = getCallClusterMock(data); + const list = await getListItem({ callCluster, id: LIST_ID, listItemIndex: LIST_INDEX }); expect(list).toEqual(null); }); }); diff --git a/x-pack/plugins/lists/server/services/items/get_list_item.ts b/x-pack/plugins/lists/server/services/items/get_list_item.ts index 1c91b698016483..83b30d336ccd47 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item.ts @@ -5,36 +5,33 @@ */ import { SearchResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; import { Id, ListItemSchema, SearchEsListItemSchema } from '../../../common/schemas'; -import { DataClient } from '../../types'; import { deriveTypeFromItem, transformElasticToListItem } from '../utils'; interface GetListItemOptions { id: Id; - dataClient: DataClient; + callCluster: APICaller; listItemIndex: string; } export const getListItem = async ({ id, - dataClient, + callCluster, listItemIndex, }: GetListItemOptions): Promise => { - const listItemES: SearchResponse = await dataClient.callAsCurrentUser( - 'search', - { - body: { - query: { - term: { - _id: id, - }, + const listItemES: SearchResponse = await callCluster('search', { + body: { + query: { + term: { + _id: id, }, }, - ignoreUnavailable: true, - index: listItemIndex, - } - ); + }, + ignoreUnavailable: true, + index: listItemIndex, + }); if (listItemES.hits.hits.length) { const type = deriveTypeFromItem({ item: listItemES.hits.hits[0]._source }); diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts index a6efcbc0d3ffb9..49bcf12043d7c9 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts @@ -4,14 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import { APICaller } from 'kibana/server'; + import { ListItemArraySchema, Type } from '../../../common/schemas'; -import { DataClient } from '../../types'; import { getListItemByValues } from '.'; export interface GetListItemByValueOptions { listId: string; - dataClient: DataClient; + callCluster: APICaller; listItemIndex: string; type: Type; value: string; @@ -19,13 +20,13 @@ export interface GetListItemByValueOptions { export const getListItemByValue = async ({ listId, - dataClient, + callCluster, listItemIndex, type, value, }: GetListItemByValueOptions): Promise => getListItemByValues({ - dataClient, + callCluster, listId, listItemIndex, type, diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts index 55b170487d95a4..7f5fff4dc3147a 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts @@ -4,8 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE, VALUE_2, getDataClientMock } from '../mocks'; -import { getSearchListItemMock } from '../mocks/get_search_list_item_mock'; +import { + LIST_ID, + LIST_ITEM_INDEX, + TYPE, + VALUE, + VALUE_2, + getCallClusterMock, + getSearchListItemMock, +} from '../mocks'; import { getListItemByValues } from './get_list_item_by_values'; @@ -21,27 +28,29 @@ describe('get_list_item_by_values', () => { test('Returns a an empty array if the ES query is also empty', async () => { const data = getSearchListItemMock(); data.hits.hits = []; - const dataClient = getDataClientMock(data); + const callCluster = getCallClusterMock(data); const listItem = await getListItemByValues({ - dataClient, + callCluster, listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, type: TYPE, value: [VALUE, VALUE_2], }); + expect(listItem).toEqual([]); }); test('Returns transformed list item if the data exists within ES', async () => { const data = getSearchListItemMock(); - const dataClient = getDataClientMock(data); + const callCluster = getCallClusterMock(data); const listItem = await getListItemByValues({ - dataClient, + callCluster, listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, type: TYPE, value: [VALUE, VALUE_2], }); + expect(listItem).toEqual([ { created_at: '2020-04-20T15:25:31.830Z', diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts index 1e5c0b4a6655cc..29b9b017540270 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts @@ -5,14 +5,14 @@ */ import { SearchResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; import { ListItemArraySchema, SearchEsListItemSchema, Type } from '../../../common/schemas'; -import { DataClient } from '../../types'; import { getQueryFilterFromTypeValue, transformElasticToListItem } from '../utils'; export interface GetListItemByValuesOptions { listId: string; - dataClient: DataClient; + callCluster: APICaller; listItemIndex: string; type: Type; value: string[]; @@ -20,25 +20,22 @@ export interface GetListItemByValuesOptions { export const getListItemByValues = async ({ listId, - dataClient, + callCluster, listItemIndex, type, value, }: GetListItemByValuesOptions): Promise => { - const response: SearchResponse = await dataClient.callAsCurrentUser( - 'search', - { - body: { - query: { - bool: { - filter: getQueryFilterFromTypeValue({ listId, type, value }), - }, + const response: SearchResponse = await callCluster('search', { + body: { + query: { + bool: { + filter: getQueryFilterFromTypeValue({ listId, type, value }), }, }, - ignoreUnavailable: true, - index: listItemIndex, - size: value.length, // This has a limit on the number which is 10k - } - ); + }, + ignoreUnavailable: true, + index: listItemIndex, + size: value.length, // This has a limit on the number which is 10k + }); return transformElasticToListItem({ response, type }); }; diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_index.test.ts b/x-pack/plugins/lists/server/services/items/get_list_item_index.test.ts index 0ea8320e966bdf..ffe2eff9f3ca70 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_index.test.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_index.test.ts @@ -4,17 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { httpServerMock } from 'src/core/server/mocks'; -import { KibanaRequest } from 'src/core/server'; - -import { getSpace } from '../utils'; - import { getListItemIndex } from './get_list_item_index'; -jest.mock('../utils', () => ({ - getSpace: jest.fn(), -})); - describe('get_list_item_index', () => { beforeEach(() => { jest.clearAllMocks(); @@ -25,13 +16,9 @@ describe('get_list_item_index', () => { }); test('Returns the list item index when there is a space', async () => { - ((getSpace as unknown) as jest.Mock).mockReturnValueOnce('test-space'); - const rawRequest = httpServerMock.createRawRequest({}); - const request = KibanaRequest.from(rawRequest); const listIndex = getListItemIndex({ listsItemsIndexName: 'lists-items-index', - request, - spaces: undefined, + spaceId: 'test-space', }); expect(listIndex).toEqual('lists-items-index-test-space'); }); diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_index.ts b/x-pack/plugins/lists/server/services/items/get_list_item_index.ts index c9f1bfd4d44e46..4cd93df6d9bf42 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_index.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_index.ts @@ -4,19 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest } from 'kibana/server'; - -import { SpacesServiceSetup } from '../../../../spaces/server'; -import { getSpace } from '../utils'; - -interface GetListItemIndexOptions { - spaces: SpacesServiceSetup | undefined | null; - request: KibanaRequest; +export interface GetListItemIndexOptions { + spaceId: string; listsItemsIndexName: string; } export const getListItemIndex = ({ - spaces, - request, + spaceId, listsItemsIndexName, -}: GetListItemIndexOptions): string => `${listsItemsIndexName}-${getSpace({ request, spaces })}`; +}: GetListItemIndexOptions): string => `${listsItemsIndexName}-${spaceId}`; diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.ts b/x-pack/plugins/lists/server/services/items/update_list_item.ts index ce4f8125d77af3..6a71b2a0caf415 100644 --- a/x-pack/plugins/lists/server/services/items/update_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/update_list_item.ts @@ -5,6 +5,7 @@ */ import { CreateDocumentResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; import { Id, @@ -13,14 +14,13 @@ import { UpdateEsListItemSchema, } from '../../../common/schemas'; import { transformListItemToElasticQuery } from '../utils'; -import { DataClient } from '../../types'; import { getListItem } from './get_list_item'; export interface UpdateListItemOptions { id: Id; value: string | null | undefined; - dataClient: DataClient; + callCluster: APICaller; listItemIndex: string; user: string; meta: MetaOrUndefined; @@ -30,14 +30,14 @@ export interface UpdateListItemOptions { export const updateListItem = async ({ id, value, - dataClient, + callCluster, listItemIndex, user, meta, dateNow, }: UpdateListItemOptions): Promise => { const updatedAt = dateNow ?? new Date().toISOString(); - const listItem = await getListItem({ dataClient, id, listItemIndex }); + const listItem = await getListItem({ callCluster, id, listItemIndex }); if (listItem == null) { return null; } else { @@ -48,7 +48,7 @@ export const updateListItem = async ({ ...transformListItemToElasticQuery({ type: listItem.type, value: value ?? listItem.value }), }; - const response: CreateDocumentResponse = await dataClient.callAsCurrentUser('update', { + const response: CreateDocumentResponse = await callCluster('update', { body: { doc, }, diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts index 1fe1023e28ab9f..542c2bb12d8e51 100644 --- a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts +++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts @@ -6,8 +6,9 @@ import { Readable } from 'stream'; +import { APICaller } from 'kibana/server'; + import { MetaOrUndefined, Type } from '../../../common/schemas'; -import { DataClient } from '../../types'; import { BufferLines } from './buffer_lines'; import { getListItemByValues } from './get_list_item_by_values'; @@ -16,7 +17,7 @@ import { createListItemsBulk } from './create_list_items_bulk'; export interface ImportListItemsToStreamOptions { listId: string; stream: Readable; - dataClient: DataClient; + callCluster: APICaller; listItemIndex: string; type: Type; user: string; @@ -26,7 +27,7 @@ export interface ImportListItemsToStreamOptions { export const importListItemsToStream = ({ listId, stream, - dataClient, + callCluster, listItemIndex, type, user, @@ -37,7 +38,7 @@ export const importListItemsToStream = ({ readBuffer.on('lines', async (lines: string[]) => { await writeBufferToItems({ buffer: lines, - dataClient, + callCluster, listId, listItemIndex, meta, @@ -54,7 +55,7 @@ export const importListItemsToStream = ({ export interface WriteBufferToItemsOptions { listId: string; - dataClient: DataClient; + callCluster: APICaller; listItemIndex: string; buffer: string[]; type: Type; @@ -69,7 +70,7 @@ export interface LinesResult { export const writeBufferToItems = async ({ listId, - dataClient, + callCluster, listItemIndex, buffer, type, @@ -77,7 +78,7 @@ export const writeBufferToItems = async ({ meta, }: WriteBufferToItemsOptions): Promise => { const items = await getListItemByValues({ - dataClient, + callCluster, listId, listItemIndex, type, @@ -89,7 +90,7 @@ export const writeBufferToItems = async ({ const linesProcessed = duplicatesRemoved.length; const duplicatesFound = buffer.length - duplicatesRemoved.length; await createListItemsBulk({ - dataClient, + callCluster, listId, listItemIndex, meta, diff --git a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts index 63e9aeb61bad06..b08e5fa688b4b0 100644 --- a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts +++ b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts @@ -7,13 +7,13 @@ import { LIST_ID, LIST_ITEM_INDEX, - getDataClientMock, + getCallClusterMock, getExportListItemsToStreamOptionsMock, getResponseOptionsMock, + getSearchListItemMock, getWriteNextResponseOptions, getWriteResponseHitsToStreamOptionsMock, } from '../mocks'; -import { getSearchListItemMock } from '../mocks/get_search_list_item_mock'; import { exportListItemsToStream, @@ -37,7 +37,7 @@ describe('write_list_items_to_stream', () => { const options = getExportListItemsToStreamOptionsMock(); const firstResponse = getSearchListItemMock(); firstResponse.hits.hits = []; - options.dataClient = getDataClientMock(firstResponse); + options.callCluster = getCallClusterMock(firstResponse); exportListItemsToStream(options); let chunks: string[] = []; @@ -71,7 +71,7 @@ describe('write_list_items_to_stream', () => { const firstResponse = getSearchListItemMock(); const secondResponse = getSearchListItemMock(); firstResponse.hits.hits = [...firstResponse.hits.hits, ...secondResponse.hits.hits]; - options.dataClient = getDataClientMock(firstResponse); + options.callCluster = getCallClusterMock(firstResponse); exportListItemsToStream(options); let chunks: string[] = []; @@ -90,15 +90,14 @@ describe('write_list_items_to_stream', () => { const firstResponse = getSearchListItemMock(); firstResponse.hits.hits[0].sort = ['some-sort-value']; + const secondResponse = getSearchListItemMock(); secondResponse.hits.hits[0]._source.ip = '255.255.255.255'; - const jestCalls = jest.fn().mockResolvedValueOnce(firstResponse); - jestCalls.mockResolvedValueOnce(secondResponse); - - const dataClient = getDataClientMock(firstResponse); - dataClient.callAsCurrentUser = jestCalls; - options.dataClient = dataClient; + options.callCluster = jest + .fn() + .mockResolvedValueOnce(firstResponse) + .mockResolvedValueOnce(secondResponse); exportListItemsToStream(options); @@ -125,7 +124,7 @@ describe('write_list_items_to_stream', () => { const listItem = getSearchListItemMock(); listItem.hits.hits[0].sort = ['sort-value-1']; const options = getWriteNextResponseOptions(); - options.dataClient = getDataClientMock(listItem); + options.callCluster = getCallClusterMock(listItem); const searchAfter = await writeNextResponse(options); expect(searchAfter).toEqual(['sort-value-1']); }); @@ -134,7 +133,7 @@ describe('write_list_items_to_stream', () => { const listItem = getSearchListItemMock(); listItem.hits.hits = []; const options = getWriteNextResponseOptions(); - options.dataClient = getDataClientMock(listItem); + options.callCluster = getCallClusterMock(listItem); const searchAfter = await writeNextResponse(options); expect(searchAfter).toEqual(undefined); }); @@ -187,7 +186,7 @@ describe('write_list_items_to_stream', () => { index: LIST_ITEM_INDEX, size: 100, }; - expect(options.dataClient.callAsCurrentUser).toBeCalledWith('search', expected); + expect(options.callCluster).toBeCalledWith('search', expected); }); test('It returns a simple response with expected values and size changed', async () => { @@ -205,7 +204,7 @@ describe('write_list_items_to_stream', () => { index: LIST_ITEM_INDEX, size: 33, }; - expect(options.dataClient.callAsCurrentUser).toBeCalledWith('search', expected); + expect(options.callCluster).toBeCalledWith('search', expected); }); }); diff --git a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts index 0e0ae7b924e17a..b81e4a4fc73c20 100644 --- a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts +++ b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts @@ -7,9 +7,9 @@ import { PassThrough } from 'stream'; import { SearchResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; import { SearchEsListItemSchema } from '../../../common/schemas'; -import { DataClient } from '../../types'; import { ErrorWithStatusCode } from '../../error_with_status_code'; /** @@ -20,7 +20,7 @@ export const SIZE = 100; export interface ExportListItemsToStreamOptions { listId: string; - dataClient: DataClient; + callCluster: APICaller; listItemIndex: string; stream: PassThrough; stringToAppend: string | null | undefined; @@ -28,7 +28,7 @@ export interface ExportListItemsToStreamOptions { export const exportListItemsToStream = ({ listId, - dataClient, + callCluster, stream, listItemIndex, stringToAppend, @@ -37,7 +37,7 @@ export const exportListItemsToStream = ({ // and prevent the async await from bubbling up to the caller setTimeout(async () => { let searchAfter = await writeNextResponse({ - dataClient, + callCluster, listId, listItemIndex, searchAfter: undefined, @@ -46,7 +46,7 @@ export const exportListItemsToStream = ({ }); while (searchAfter != null) { searchAfter = await writeNextResponse({ - dataClient, + callCluster, listId, listItemIndex, searchAfter, @@ -60,7 +60,7 @@ export const exportListItemsToStream = ({ export interface WriteNextResponseOptions { listId: string; - dataClient: DataClient; + callCluster: APICaller; listItemIndex: string; stream: PassThrough; searchAfter: string[] | undefined; @@ -69,14 +69,14 @@ export interface WriteNextResponseOptions { export const writeNextResponse = async ({ listId, - dataClient, + callCluster, stream, listItemIndex, searchAfter, stringToAppend, }: WriteNextResponseOptions): Promise => { const response = await getResponse({ - dataClient, + callCluster, listId, listItemIndex, searchAfter, @@ -100,7 +100,7 @@ export const getSearchAfterFromResponse = ({ : undefined; export interface GetResponseOptions { - dataClient: DataClient; + callCluster: APICaller; listId: string; searchAfter: undefined | string[]; listItemIndex: string; @@ -108,13 +108,13 @@ export interface GetResponseOptions { } export const getResponse = async ({ - dataClient, + callCluster, searchAfter, listId, listItemIndex, size = SIZE, }: GetResponseOptions): Promise> => { - return dataClient.callAsCurrentUser('search', { + return callCluster('search', { body: { query: { term: { diff --git a/x-pack/plugins/lists/server/services/lists/client.ts b/x-pack/plugins/lists/server/services/lists/client.ts index 32578fc739f265..ba22bf72cc18c3 100644 --- a/x-pack/plugins/lists/server/services/lists/client.ts +++ b/x-pack/plugins/lists/server/services/lists/client.ts @@ -4,10 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest, ScopedClusterClient } from 'src/core/server'; +import { APICaller } from 'kibana/server'; -import { SecurityPluginSetup } from '../../../../security/server'; -import { SpacesServiceSetup } from '../../../../spaces/server'; import { ListItemArraySchema, ListItemSchema, ListSchema } from '../../../common/schemas'; import { ConfigType } from '../../config'; import { @@ -31,7 +29,6 @@ import { importListItemsToStream, updateListItem, } from '../../services/items'; -import { getUser } from '../../services/utils'; import { createBootstrapIndex, deleteAllIndex, @@ -64,47 +61,39 @@ import { UpdateListOptions, } from './client_types'; -// TODO: Consider an interface and a factory export class ListClient { - private readonly spaces: SpacesServiceSetup | undefined | null; + private readonly spaceId: string; + private readonly user: string; private readonly config: ConfigType; - private readonly dataClient: Pick< - ScopedClusterClient, - 'callAsCurrentUser' | 'callAsInternalUser' - >; - private readonly request: KibanaRequest; - private readonly security: SecurityPluginSetup; - - constructor({ request, spaces, config, dataClient, security }: ConstructorOptions) { - this.request = request; - this.spaces = spaces; + private readonly callCluster: APICaller; + + constructor({ spaceId, user, config, callCluster }: ConstructorOptions) { + this.spaceId = spaceId; + this.user = user; this.config = config; - this.dataClient = dataClient; - this.security = security; + this.callCluster = callCluster; } public getListIndex = (): string => { const { - spaces, - request, + spaceId, config: { listIndex: listsIndexName }, } = this; - return getListIndex({ listsIndexName, request, spaces }); + return getListIndex({ listsIndexName, spaceId }); }; public getListItemIndex = (): string => { const { - spaces, - request, + spaceId, config: { listItemIndex: listsItemsIndexName }, } = this; - return getListItemIndex({ listsItemsIndexName, request, spaces }); + return getListItemIndex({ listsItemsIndexName, spaceId }); }; public getList = async ({ id }: GetListOptions): Promise => { - const { dataClient } = this; + const { callCluster } = this; const listIndex = this.getListIndex(); - return getList({ dataClient, id, listIndex }); + return getList({ callCluster, id, listIndex }); }; public createList = async ({ @@ -114,10 +103,9 @@ export class ListClient { type, meta, }: CreateListOptions): Promise => { - const { dataClient, security, request } = this; + const { callCluster, user } = this; const listIndex = this.getListIndex(); - const user = getUser({ request, security }); - return createList({ dataClient, description, id, listIndex, meta, name, type, user }); + return createList({ callCluster, description, id, listIndex, meta, name, type, user }); }; public createListIfItDoesNotExist = async ({ @@ -136,67 +124,51 @@ export class ListClient { }; public getListIndexExists = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listIndex = this.getListIndex(); - return getIndexExists(callAsCurrentUser, listIndex); + return getIndexExists(callCluster, listIndex); }; public getListItemIndexExists = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); - return getIndexExists(callAsCurrentUser, listItemIndex); + return getIndexExists(callCluster, listItemIndex); }; public createListBootStrapIndex = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listIndex = this.getListIndex(); - return createBootstrapIndex(callAsCurrentUser, listIndex); + return createBootstrapIndex(callCluster, listIndex); }; public createListItemBootStrapIndex = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); - return createBootstrapIndex(callAsCurrentUser, listItemIndex); + return createBootstrapIndex(callCluster, listItemIndex); }; public getListPolicyExists = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listIndex = this.getListIndex(); - return getPolicyExists(callAsCurrentUser, listIndex); + return getPolicyExists(callCluster, listIndex); }; public getListItemPolicyExists = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listsItemIndex = this.getListItemIndex(); - return getPolicyExists(callAsCurrentUser, listsItemIndex); + return getPolicyExists(callCluster, listsItemIndex); }; public getListTemplateExists = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listIndex = this.getListIndex(); - return getTemplateExists(callAsCurrentUser, listIndex); + return getTemplateExists(callCluster, listIndex); }; public getListItemTemplateExists = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); - return getTemplateExists(callAsCurrentUser, listItemIndex); + return getTemplateExists(callCluster, listItemIndex); }; public getListTemplate = (): Record => { @@ -210,91 +182,71 @@ export class ListClient { }; public setListTemplate = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const template = this.getListTemplate(); const listIndex = this.getListIndex(); - return setTemplate(callAsCurrentUser, listIndex, template); + return setTemplate(callCluster, listIndex, template); }; public setListItemTemplate = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const template = this.getListItemTemplate(); const listItemIndex = this.getListItemIndex(); - return setTemplate(callAsCurrentUser, listItemIndex, template); + return setTemplate(callCluster, listItemIndex, template); }; public setListPolicy = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listIndex = this.getListIndex(); - return setPolicy(callAsCurrentUser, listIndex, listPolicy); + return setPolicy(callCluster, listIndex, listPolicy); }; public setListItemPolicy = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); - return setPolicy(callAsCurrentUser, listItemIndex, listsItemsPolicy); + return setPolicy(callCluster, listItemIndex, listsItemsPolicy); }; public deleteListIndex = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listIndex = this.getListIndex(); - return deleteAllIndex(callAsCurrentUser, `${listIndex}-*`); + return deleteAllIndex(callCluster, `${listIndex}-*`); }; public deleteListItemIndex = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); - return deleteAllIndex(callAsCurrentUser, `${listItemIndex}-*`); + return deleteAllIndex(callCluster, `${listItemIndex}-*`); }; public deleteListPolicy = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listIndex = this.getListIndex(); - return deletePolicy(callAsCurrentUser, listIndex); + return deletePolicy(callCluster, listIndex); }; public deleteListItemPolicy = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); - return deletePolicy(callAsCurrentUser, listItemIndex); + return deletePolicy(callCluster, listItemIndex); }; public deleteListTemplate = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listIndex = this.getListIndex(); - return deleteTemplate(callAsCurrentUser, listIndex); + return deleteTemplate(callCluster, listIndex); }; public deleteListItemTemplate = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); - return deleteTemplate(callAsCurrentUser, listItemIndex); + return deleteTemplate(callCluster, listItemIndex); }; public deleteListItem = async ({ id }: DeleteListItemOptions): Promise => { - const { dataClient } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); - return deleteListItem({ dataClient, id, listItemIndex }); + return deleteListItem({ callCluster, id, listItemIndex }); }; public deleteListItemByValue = async ({ @@ -302,10 +254,10 @@ export class ListClient { value, type, }: DeleteListItemByValueOptions): Promise => { - const { dataClient } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); return deleteListItemByValue({ - dataClient, + callCluster, listId, listItemIndex, type, @@ -314,11 +266,11 @@ export class ListClient { }; public deleteList = async ({ id }: DeleteListOptions): Promise => { - const { dataClient } = this; + const { callCluster } = this; const listIndex = this.getListIndex(); const listItemIndex = this.getListItemIndex(); return deleteList({ - dataClient, + callCluster, id, listIndex, listItemIndex, @@ -330,10 +282,10 @@ export class ListClient { listId, stream, }: ExportListItemsToStreamOptions): void => { - const { dataClient } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); exportListItemsToStream({ - dataClient, + callCluster, listId, listItemIndex, stream, @@ -347,11 +299,10 @@ export class ListClient { stream, meta, }: ImportListItemsToStreamOptions): Promise => { - const { dataClient, security, request } = this; + const { callCluster, user } = this; const listItemIndex = this.getListItemIndex(); - const user = getUser({ request, security }); return importListItemsToStream({ - dataClient, + callCluster, listId, listItemIndex, meta, @@ -366,10 +317,10 @@ export class ListClient { value, type, }: GetListItemByValueOptions): Promise => { - const { dataClient } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); return getListItemByValue({ - dataClient, + callCluster, listId, listItemIndex, type, @@ -384,11 +335,10 @@ export class ListClient { type, meta, }: CreateListItemOptions): Promise => { - const { dataClient, security, request } = this; + const { callCluster, user } = this; const listItemIndex = this.getListItemIndex(); - const user = getUser({ request, security }); return createListItem({ - dataClient, + callCluster, id, listId, listItemIndex, @@ -404,11 +354,10 @@ export class ListClient { value, meta, }: UpdateListItemOptions): Promise => { - const { dataClient, security, request } = this; - const user = getUser({ request, security }); + const { callCluster, user } = this; const listItemIndex = this.getListItemIndex(); return updateListItem({ - dataClient, + callCluster, id, listItemIndex, meta, @@ -423,11 +372,10 @@ export class ListClient { description, meta, }: UpdateListOptions): Promise => { - const { dataClient, security, request } = this; - const user = getUser({ request, security }); + const { callCluster, user } = this; const listIndex = this.getListIndex(); return updateList({ - dataClient, + callCluster, description, id, listIndex, @@ -438,10 +386,10 @@ export class ListClient { }; public getListItem = async ({ id }: GetListItemOptions): Promise => { - const { dataClient } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); return getListItem({ - dataClient, + callCluster, id, listItemIndex, }); @@ -452,10 +400,10 @@ export class ListClient { listId, value, }: GetListItemsByValueOptions): Promise => { - const { dataClient } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); return getListItemByValues({ - dataClient, + callCluster, listId, listItemIndex, type, diff --git a/x-pack/plugins/lists/server/services/lists/client_types.ts b/x-pack/plugins/lists/server/services/lists/client_types.ts index c3b6a484d87876..2cc58c02dbfcff 100644 --- a/x-pack/plugins/lists/server/services/lists/client_types.ts +++ b/x-pack/plugins/lists/server/services/lists/client_types.ts @@ -6,10 +6,9 @@ import { PassThrough, Readable } from 'stream'; -import { KibanaRequest } from 'kibana/server'; +import { APICaller, KibanaRequest } from 'kibana/server'; import { SecurityPluginSetup } from '../../../../security/server'; -import { SpacesServiceSetup } from '../../../../spaces/server'; import { Description, DescriptionOrUndefined, @@ -21,14 +20,14 @@ import { Type, } from '../../../common/schemas'; import { ConfigType } from '../../config'; -import { DataClient } from '../../types'; export interface ConstructorOptions { + callCluster: APICaller; config: ConfigType; - dataClient: DataClient; request: KibanaRequest; - spaces: SpacesServiceSetup | undefined | null; - security: SecurityPluginSetup; + spaceId: string; + user: string; + security: SecurityPluginSetup | undefined | null; } export interface GetListOptions { diff --git a/x-pack/plugins/lists/server/services/lists/create_list.test.ts b/x-pack/plugins/lists/server/services/lists/create_list.test.ts index d6ba435155c60f..36284a70fb97df 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list.test.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list.test.ts @@ -31,7 +31,7 @@ describe('crete_list', () => { expect(list).toEqual(expected); }); - test('It calls "callAsCurrentUser" with body, index, and listIndex', async () => { + test('It calls "callCluster" with body, index, and listIndex', async () => { const options = getCreateListOptionsMock(); await createList(options); const body = getIndexESListMock(); @@ -40,7 +40,7 @@ describe('crete_list', () => { id: LIST_ID, index: LIST_INDEX, }; - expect(options.dataClient.callAsCurrentUser).toBeCalledWith('index', expected); + expect(options.callCluster).toBeCalledWith('index', expected); }); test('It returns an auto-generated id if id is sent in undefined', async () => { diff --git a/x-pack/plugins/lists/server/services/lists/create_list.ts b/x-pack/plugins/lists/server/services/lists/create_list.ts index dcf87b3ad1ef16..ddbc99c88a877a 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list.ts @@ -6,8 +6,8 @@ import uuid from 'uuid'; import { CreateDocumentResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; -import { DataClient } from '../../types'; import { Description, IdOrUndefined, @@ -23,7 +23,7 @@ export interface CreateListOptions { type: Type; name: Name; description: Description; - dataClient: DataClient; + callCluster: APICaller; listIndex: string; user: string; meta: MetaOrUndefined; @@ -36,7 +36,7 @@ export const createList = async ({ name, type, description, - dataClient, + callCluster, listIndex, user, meta, @@ -55,7 +55,7 @@ export const createList = async ({ updated_at: createdAt, updated_by: user, }; - const response: CreateDocumentResponse = await dataClient.callAsCurrentUser('index', { + const response: CreateDocumentResponse = await callCluster('index', { body, id, index: listIndex, diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.test.ts b/x-pack/plugins/lists/server/services/lists/delete_list.test.ts index f32273e3e7f768..62b5e7c7aec4a3 100644 --- a/x-pack/plugins/lists/server/services/lists/delete_list.test.ts +++ b/x-pack/plugins/lists/server/services/lists/delete_list.test.ts @@ -52,7 +52,7 @@ describe('delete_list', () => { body: { query: { term: { list_id: LIST_ID } } }, index: LIST_ITEM_INDEX, }; - expect(options.dataClient.callAsCurrentUser).toBeCalledWith('deleteByQuery', deleteByQuery); + expect(options.callCluster).toBeCalledWith('deleteByQuery', deleteByQuery); }); test('Delete calls "delete" second if a list is returned from getList', async () => { @@ -64,13 +64,13 @@ describe('delete_list', () => { id: LIST_ID, index: LIST_INDEX, }; - expect(options.dataClient.callAsCurrentUser).toHaveBeenNthCalledWith(2, 'delete', deleteQuery); + expect(options.callCluster).toHaveBeenNthCalledWith(2, 'delete', deleteQuery); }); test('Delete does not call data client if the list returns null', async () => { ((getList as unknown) as jest.Mock).mockResolvedValueOnce(null); const options = getDeleteListOptionsMock(); await deleteList(options); - expect(options.dataClient.callAsCurrentUser).not.toHaveBeenCalled(); + expect(options.callCluster).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.ts b/x-pack/plugins/lists/server/services/lists/delete_list.ts index 653a8da74a105d..bc66c88b082a31 100644 --- a/x-pack/plugins/lists/server/services/lists/delete_list.ts +++ b/x-pack/plugins/lists/server/services/lists/delete_list.ts @@ -4,29 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ +import { APICaller } from 'kibana/server'; + import { Id, ListSchema } from '../../../common/schemas'; -import { DataClient } from '../../types'; import { getList } from './get_list'; export interface DeleteListOptions { id: Id; - dataClient: DataClient; + callCluster: APICaller; listIndex: string; listItemIndex: string; } export const deleteList = async ({ id, - dataClient, + callCluster, listIndex, listItemIndex, }: DeleteListOptions): Promise => { - const list = await getList({ dataClient, id, listIndex }); + const list = await getList({ callCluster, id, listIndex }); if (list == null) { return null; } else { - await dataClient.callAsCurrentUser('deleteByQuery', { + await callCluster('deleteByQuery', { body: { query: { term: { @@ -37,7 +38,7 @@ export const deleteList = async ({ index: listItemIndex, }); - await dataClient.callAsCurrentUser('delete', { + await callCluster('delete', { id, index: listIndex, }); diff --git a/x-pack/plugins/lists/server/services/lists/get_list.test.ts b/x-pack/plugins/lists/server/services/lists/get_list.test.ts index 1f9a33c191764f..c997d5325296a6 100644 --- a/x-pack/plugins/lists/server/services/lists/get_list.test.ts +++ b/x-pack/plugins/lists/server/services/lists/get_list.test.ts @@ -7,7 +7,7 @@ import { LIST_ID, LIST_INDEX, - getDataClientMock, + getCallClusterMock, getListResponseMock, getSearchListMock, } from '../mocks'; @@ -25,8 +25,8 @@ describe('get_list', () => { test('it returns a list as expected if the list is found', async () => { const data = getSearchListMock(); - const dataClient = getDataClientMock(data); - const list = await getList({ dataClient, id: LIST_ID, listIndex: LIST_INDEX }); + const callCluster = getCallClusterMock(data); + const list = await getList({ callCluster, id: LIST_ID, listIndex: LIST_INDEX }); const expected = getListResponseMock(); expect(list).toEqual(expected); }); @@ -34,8 +34,8 @@ describe('get_list', () => { test('it returns null if the search is empty', async () => { const data = getSearchListMock(); data.hits.hits = []; - const dataClient = getDataClientMock(data); - const list = await getList({ dataClient, id: LIST_ID, listIndex: LIST_INDEX }); + const callCluster = getCallClusterMock(data); + const list = await getList({ callCluster, id: LIST_ID, listIndex: LIST_INDEX }); expect(list).toEqual(null); }); }); diff --git a/x-pack/plugins/lists/server/services/lists/get_list.ts b/x-pack/plugins/lists/server/services/lists/get_list.ts index 216703f08f0697..c04bd504ad8c01 100644 --- a/x-pack/plugins/lists/server/services/lists/get_list.ts +++ b/x-pack/plugins/lists/server/services/lists/get_list.ts @@ -5,22 +5,22 @@ */ import { SearchResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; import { Id, ListSchema, SearchEsListSchema } from '../../../common/schemas'; -import { DataClient } from '../../types'; interface GetListOptions { id: Id; - dataClient: DataClient; + callCluster: APICaller; listIndex: string; } export const getList = async ({ id, - dataClient, + callCluster, listIndex, }: GetListOptions): Promise => { - const result: SearchResponse = await dataClient.callAsCurrentUser('search', { + const result: SearchResponse = await callCluster('search', { body: { query: { term: { diff --git a/x-pack/plugins/lists/server/services/lists/get_list_index.test.ts b/x-pack/plugins/lists/server/services/lists/get_list_index.test.ts index 22a738a340b25d..f82928ffeddd2f 100644 --- a/x-pack/plugins/lists/server/services/lists/get_list_index.test.ts +++ b/x-pack/plugins/lists/server/services/lists/get_list_index.test.ts @@ -4,17 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { httpServerMock } from 'src/core/server/mocks'; -import { KibanaRequest } from 'src/core/server'; - -import { getSpace } from '../utils'; - import { getListIndex } from './get_list_index'; -jest.mock('../utils', () => ({ - getSpace: jest.fn(), -})); - describe('get_list_index', () => { beforeEach(() => { jest.clearAllMocks(); @@ -25,13 +16,9 @@ describe('get_list_index', () => { }); test('Returns the list index when there is a space', async () => { - ((getSpace as unknown) as jest.Mock).mockReturnValueOnce('test-space'); - const rawRequest = httpServerMock.createRawRequest({}); - const request = KibanaRequest.from(rawRequest); const listIndex = getListIndex({ listsIndexName: 'lists-index', - request, - spaces: undefined, + spaceId: 'test-space', }); expect(listIndex).toEqual('lists-index-test-space'); }); diff --git a/x-pack/plugins/lists/server/services/lists/get_list_index.ts b/x-pack/plugins/lists/server/services/lists/get_list_index.ts index 70b85fc97ebfa1..5086603fa84035 100644 --- a/x-pack/plugins/lists/server/services/lists/get_list_index.ts +++ b/x-pack/plugins/lists/server/services/lists/get_list_index.ts @@ -4,16 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest } from 'kibana/server'; - -import { SpacesServiceSetup } from '../../../../spaces/server'; -import { getSpace } from '../utils'; - interface GetListIndexOptions { - spaces: SpacesServiceSetup | undefined | null; - request: KibanaRequest; + spaceId: string; listsIndexName: string; } -export const getListIndex = ({ spaces, request, listsIndexName }: GetListIndexOptions): string => - `${listsIndexName}-${getSpace({ request, spaces })}`; +export const getListIndex = ({ spaceId, listsIndexName }: GetListIndexOptions): string => + `${listsIndexName}-${spaceId}`; diff --git a/x-pack/plugins/lists/server/services/lists/update_list.ts b/x-pack/plugins/lists/server/services/lists/update_list.ts index 55f110e9a8291f..9859adf0624857 100644 --- a/x-pack/plugins/lists/server/services/lists/update_list.ts +++ b/x-pack/plugins/lists/server/services/lists/update_list.ts @@ -5,6 +5,7 @@ */ import { CreateDocumentResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; import { DescriptionOrUndefined, @@ -14,13 +15,12 @@ import { NameOrUndefined, UpdateEsListSchema, } from '../../../common/schemas'; -import { DataClient } from '../../types'; import { getList } from '.'; export interface UpdateListOptions { id: Id; - dataClient: DataClient; + callCluster: APICaller; listIndex: string; user: string; name: NameOrUndefined; @@ -33,14 +33,14 @@ export const updateList = async ({ id, name, description, - dataClient, + callCluster, listIndex, user, meta, dateNow, }: UpdateListOptions): Promise => { const updatedAt = dateNow ?? new Date().toISOString(); - const list = await getList({ dataClient, id, listIndex }); + const list = await getList({ callCluster, id, listIndex }); if (list == null) { return null; } else { @@ -51,7 +51,7 @@ export const updateList = async ({ updated_at: updatedAt, updated_by: user, }; - const response: CreateDocumentResponse = await dataClient.callAsCurrentUser('update', { + const response: CreateDocumentResponse = await callCluster('update', { body: { doc }, id, index: listIndex, diff --git a/x-pack/plugins/lists/server/services/mocks/get_data_client_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_call_cluster_mock.ts similarity index 57% rename from x-pack/plugins/lists/server/services/mocks/get_data_client_mock.ts rename to x-pack/plugins/lists/server/services/mocks/get_call_cluster_mock.ts index 6e4cc40efeed73..180ecbb7973392 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_data_client_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_call_cluster_mock.ts @@ -5,15 +5,11 @@ */ import { CreateDocumentResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; import { LIST_INDEX } from './lists_services_mock_constants'; import { getShardMock } from './get_shard_mock'; -interface DataClientReturn { - callAsCurrentUser: () => Promise; - callAsInternalUser: () => Promise; -} - export const getEmptyCreateDocumentResponseMock = (): CreateDocumentResponse => ({ _id: 'elastic-id-123', _index: LIST_INDEX, @@ -24,11 +20,6 @@ export const getEmptyCreateDocumentResponseMock = (): CreateDocumentResponse => result: '', }); -export const getDataClientMock = ( - callAsCurrentUserData: unknown = getEmptyCreateDocumentResponseMock() -): DataClientReturn => ({ - callAsCurrentUser: jest.fn().mockResolvedValue(callAsCurrentUserData), - callAsInternalUser: (): Promise => { - throw new Error('This function should not be calling "callAsInternalUser"'); - }, -}); +export const getCallClusterMock = ( + callCluster: unknown = getEmptyCreateDocumentResponseMock() +): APICaller => jest.fn().mockResolvedValue(callCluster); diff --git a/x-pack/plugins/lists/server/services/mocks/get_create_list_item_bulk_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_create_list_item_bulk_options_mock.ts index 0f4d92cabaa7a1..fcdad66d652518 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_create_list_item_bulk_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_create_list_item_bulk_options_mock.ts @@ -6,7 +6,7 @@ import { CreateListItemsBulkOptions } from '../items'; -import { getDataClientMock } from './get_data_client_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; import { DATE_NOW, LIST_ID, @@ -20,7 +20,7 @@ import { } from './lists_services_mock_constants'; export const getCreateListItemBulkOptionsMock = (): CreateListItemsBulkOptions => ({ - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), dateNow: DATE_NOW, listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, diff --git a/x-pack/plugins/lists/server/services/mocks/get_create_list_item_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_create_list_item_options_mock.ts index 960db293f11245..17e3ad2f8de083 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_create_list_item_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_create_list_item_options_mock.ts @@ -6,7 +6,7 @@ import { CreateListItemOptions } from '../items'; -import { getDataClientMock } from './get_data_client_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; import { DATE_NOW, LIST_ID, @@ -19,7 +19,7 @@ import { } from './lists_services_mock_constants'; export const getCreateListItemOptionsMock = (): CreateListItemOptions => ({ - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), dateNow: DATE_NOW, id: LIST_ITEM_ID, listId: LIST_ID, diff --git a/x-pack/plugins/lists/server/services/mocks/get_create_list_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_create_list_options_mock.ts index 1a005a76547f5c..0ea6533fc122a8 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_create_list_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_create_list_options_mock.ts @@ -6,7 +6,7 @@ import { CreateListOptions } from '../lists'; -import { getDataClientMock } from './get_data_client_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; import { DATE_NOW, DESCRIPTION, @@ -20,7 +20,7 @@ import { } from './lists_services_mock_constants'; export const getCreateListOptionsMock = (): CreateListOptions => ({ - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), dateNow: DATE_NOW, description: DESCRIPTION, id: LIST_ID, diff --git a/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_by_value_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_by_value_options_mock.ts index 58fd319589ea3d..f6859e72d71b35 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_by_value_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_by_value_options_mock.ts @@ -6,11 +6,11 @@ import { DeleteListItemByValueOptions } from '../items'; -import { getDataClientMock } from './get_data_client_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE } from './lists_services_mock_constants'; export const getDeleteListItemByValueOptionsMock = (): DeleteListItemByValueOptions => ({ - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, type: TYPE, diff --git a/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_options_mock.ts index 1e7167547a6de9..271c185860b074 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_options_mock.ts @@ -6,11 +6,11 @@ import { DeleteListItemOptions } from '../items'; -import { getDataClientMock } from './get_data_client_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; import { LIST_ITEM_ID, LIST_ITEM_INDEX } from './lists_services_mock_constants'; export const getDeleteListItemOptionsMock = (): DeleteListItemOptions => ({ - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), id: LIST_ITEM_ID, listItemIndex: LIST_ITEM_INDEX, }); diff --git a/x-pack/plugins/lists/server/services/mocks/get_delete_list_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_delete_list_options_mock.ts index 9d70dae969362d..8ec92dfa4ef775 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_delete_list_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_delete_list_options_mock.ts @@ -6,11 +6,11 @@ import { DeleteListOptions } from '../lists'; -import { getDataClientMock } from './get_data_client_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; import { LIST_ID, LIST_INDEX, LIST_ITEM_INDEX } from './lists_services_mock_constants'; export const getDeleteListOptionsMock = (): DeleteListOptions => ({ - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), id: LIST_ID, listIndex: LIST_INDEX, listItemIndex: LIST_ITEM_INDEX, diff --git a/x-pack/plugins/lists/server/services/mocks/get_import_list_items_to_stream_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_import_list_items_to_stream_options_mock.ts index 4cc6d85cd947ab..d7541f3e09e6cd 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_import_list_items_to_stream_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_import_list_items_to_stream_options_mock.ts @@ -5,12 +5,12 @@ */ import { ImportListItemsToStreamOptions } from '../items'; -import { getDataClientMock } from './get_data_client_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; import { LIST_ID, LIST_ITEM_INDEX, META, TYPE, USER } from './lists_services_mock_constants'; import { TestReadable } from './test_readable'; export const getImportListItemsToStreamOptionsMock = (): ImportListItemsToStreamOptions => ({ - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, meta: META, diff --git a/x-pack/plugins/lists/server/services/mocks/get_list_item_by_value_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_list_item_by_value_options_mock.ts index ab1bde48e7ebf9..96bc22ca7e6f27 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_list_item_by_value_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_list_item_by_value_options_mock.ts @@ -6,11 +6,11 @@ import { GetListItemByValueOptions } from '../items'; -import { getDataClientMock } from './get_data_client_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE } from './lists_services_mock_constants'; export const getListItemByValueOptionsMocks = (): GetListItemByValueOptions => ({ - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, type: TYPE, diff --git a/x-pack/plugins/lists/server/services/mocks/get_list_item_by_values_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_list_item_by_values_options_mock.ts index c15d417d10289c..f21f97dc8d15f2 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_list_item_by_values_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_list_item_by_values_options_mock.ts @@ -6,11 +6,11 @@ import { GetListItemByValuesOptions } from '../items'; -import { getDataClientMock } from './get_data_client_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE, VALUE_2 } from './lists_services_mock_constants'; export const getListItemByValuesOptionsMocks = (): GetListItemByValuesOptions => ({ - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, type: TYPE, diff --git a/x-pack/plugins/lists/server/services/mocks/get_update_list_item_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_update_list_item_options_mock.ts index b60d6f5113e06b..0555997941baa0 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_update_list_item_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_update_list_item_options_mock.ts @@ -5,7 +5,7 @@ */ import { UpdateListItemOptions } from '../items'; -import { getDataClientMock } from './get_data_client_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; import { DATE_NOW, LIST_ITEM_ID, @@ -16,7 +16,7 @@ import { } from './lists_services_mock_constants'; export const getUpdateListItemOptionsMock = (): UpdateListItemOptions => ({ - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), dateNow: DATE_NOW, id: LIST_ITEM_ID, listItemIndex: LIST_ITEM_INDEX, diff --git a/x-pack/plugins/lists/server/services/mocks/get_update_list_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_update_list_options_mock.ts index e56ebc24bdae1e..fe6fc37eaf81e6 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_update_list_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_update_list_options_mock.ts @@ -5,7 +5,7 @@ */ import { UpdateListOptions } from '../lists'; -import { getDataClientMock } from './get_data_client_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; import { DATE_NOW, DESCRIPTION, @@ -17,7 +17,7 @@ import { } from './lists_services_mock_constants'; export const getUpdateListOptionsMock = (): UpdateListOptions => ({ - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), dateNow: DATE_NOW, description: DESCRIPTION, id: LIST_ID, diff --git a/x-pack/plugins/lists/server/services/mocks/get_write_buffer_to_items_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_write_buffer_to_items_options_mock.ts index 9a77453b65d6aa..d6b7d70c1aa778 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_write_buffer_to_items_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_write_buffer_to_items_options_mock.ts @@ -5,12 +5,12 @@ */ import { WriteBufferToItemsOptions } from '../items'; -import { getDataClientMock } from './get_data_client_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; import { LIST_ID, LIST_ITEM_INDEX, META, TYPE, USER } from './lists_services_mock_constants'; export const getWriteBufferToItemsOptionsMock = (): WriteBufferToItemsOptions => ({ buffer: [], - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, meta: META, diff --git a/x-pack/plugins/lists/server/services/mocks/get_write_list_items_to_stream_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_write_list_items_to_stream_options_mock.ts index 96724c2a88045c..c945818a83e8a8 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_write_list_items_to_stream_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_write_list_items_to_stream_options_mock.ts @@ -13,12 +13,12 @@ import { WriteResponseHitsToStreamOptions, } from '../items'; -import { getDataClientMock } from './get_data_client_mock'; import { LIST_ID, LIST_ITEM_INDEX } from './lists_services_mock_constants'; import { getSearchListItemMock } from './get_search_list_item_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; export const getExportListItemsToStreamOptionsMock = (): ExportListItemsToStreamOptions => ({ - dataClient: getDataClientMock(getSearchListItemMock()), + callCluster: getCallClusterMock(getSearchListItemMock()), listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, stream: new Stream.PassThrough(), @@ -26,7 +26,7 @@ export const getExportListItemsToStreamOptionsMock = (): ExportListItemsToStream }); export const getWriteNextResponseOptions = (): WriteNextResponseOptions => ({ - dataClient: getDataClientMock(getSearchListItemMock()), + callCluster: getCallClusterMock(getSearchListItemMock()), listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, searchAfter: [], @@ -35,7 +35,7 @@ export const getWriteNextResponseOptions = (): WriteNextResponseOptions => ({ }); export const getResponseOptionsMock = (): GetResponseOptions => ({ - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, searchAfter: [], diff --git a/x-pack/plugins/lists/server/services/mocks/index.ts b/x-pack/plugins/lists/server/services/mocks/index.ts index 516264149fac70..c555ba322fa2bb 100644 --- a/x-pack/plugins/lists/server/services/mocks/index.ts +++ b/x-pack/plugins/lists/server/services/mocks/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './get_data_client_mock'; +export * from './get_call_cluster_mock'; export * from './get_delete_list_options_mock'; export * from './get_create_list_options_mock'; export * from './get_list_response_mock'; @@ -27,3 +27,5 @@ export * from './get_update_list_item_options_mock'; export * from './get_write_buffer_to_items_options_mock'; export * from './get_import_list_items_to_stream_options_mock'; export * from './get_write_list_items_to_stream_options_mock'; +export * from './get_search_list_item_mock'; +export * from './test_readable'; diff --git a/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.test.ts b/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.test.ts index 6e5dca7d54e5b8..3b6f58479a2f22 100644 --- a/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.test.ts +++ b/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.test.ts @@ -10,6 +10,14 @@ import { Type } from '../../../common/schemas'; import { deriveTypeFromItem } from './derive_type_from_es_type'; describe('derive_type_from_es_type', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + test('it returns the item ip if it exists', () => { const item = getSearchEsListItemMock(); const derivedType = deriveTypeFromItem({ item }); diff --git a/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.test.ts b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.test.ts new file mode 100644 index 00000000000000..3d48e44e26eaa3 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.test.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { QueryFilterType, getQueryFilterFromTypeValue } from './get_query_filter_from_type_value'; + +describe('get_query_filter_from_type_value', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns an ip if given an ip', () => { + const queryFilter = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'ip', + value: ['127.0.0.1'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { terms: { ip: ['127.0.0.1'] } }, + ]; + expect(queryFilter).toEqual(expected); + }); + + test('it returns two ip if given two ip', () => { + const queryFilter = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'ip', + value: ['127.0.0.1', '127.0.0.2'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { terms: { ip: ['127.0.0.1', '127.0.0.2'] } }, + ]; + expect(queryFilter).toEqual(expected); + }); + + test('it returns a keyword if given a keyword', () => { + const queryFilter = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'keyword', + value: ['host-name-1'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { terms: { keyword: ['host-name-1'] } }, + ]; + expect(queryFilter).toEqual(expected); + }); + + test('it returns two keywords if given two values', () => { + const queryFilter = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'keyword', + value: ['host-name-1', 'host-name-2'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { terms: { keyword: ['host-name-1', 'host-name-2'] } }, + ]; + expect(queryFilter).toEqual(expected); + }); + + test('it returns an empty keyword given an empty value', () => { + const queryFilter = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'keyword', + value: [], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { terms: { keyword: [] } }, + ]; + expect(queryFilter).toEqual(expected); + }); + + test('it returns an empty ip given an empty value', () => { + const queryFilter = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'ip', + value: [], + }); + const expected: QueryFilterType = [{ term: { list_id: 'list-123' } }, { terms: { ip: [] } }]; + expect(queryFilter).toEqual(expected); + }); +}); diff --git a/x-pack/plugins/lists/server/services/utils/index.ts b/x-pack/plugins/lists/server/services/utils/index.ts index f256b6cb8f2d50..8a44b5ab607bf5 100644 --- a/x-pack/plugins/lists/server/services/utils/index.ts +++ b/x-pack/plugins/lists/server/services/utils/index.ts @@ -7,6 +7,4 @@ export * from './get_query_filter_from_type_value'; export * from './transform_elastic_to_list_item'; export * from './transform_list_item_to_elastic_query'; -export * from './get_user'; export * from './derive_type_from_es_type'; -export * from './get_space'; diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts new file mode 100644 index 00000000000000..3b9864be6df538 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ListItemArraySchema } from '../../../common/schemas'; +import { getListItemResponseMock, getSearchListItemMock } from '../mocks'; + +import { transformElasticToListItem } from './transform_elastic_to_list_item'; + +describe('transform_elastic_to_list_item', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it transforms an elastic type to a list item type', () => { + const response = getSearchListItemMock(); + const queryFilter = transformElasticToListItem({ + response, + type: 'ip', + }); + const expected: ListItemArraySchema = [getListItemResponseMock()]; + expect(queryFilter).toEqual(expected); + }); + + test('it transforms an elastic keyword type to a list item type', () => { + const response = getSearchListItemMock(); + response.hits.hits[0]._source.ip = undefined; + response.hits.hits[0]._source.keyword = 'host-name-example'; + const queryFilter = transformElasticToListItem({ + response, + type: 'keyword', + }); + const listItemResponse = getListItemResponseMock(); + listItemResponse.type = 'keyword'; + listItemResponse.value = 'host-name-example'; + const expected: ListItemArraySchema = [listItemResponse]; + expect(queryFilter).toEqual(expected); + }); + + test('it does a throw if it cannot determine the list item type from "ip"', () => { + const response = getSearchListItemMock(); + response.hits.hits[0]._source.ip = undefined; + response.hits.hits[0]._source.keyword = 'host-name-example'; + expect(() => + transformElasticToListItem({ + response, + type: 'ip', + }) + ).toThrow('Was expecting ip to not be null/undefined'); + }); + + test('it does a throw if it cannot determine the list item type from "keyword"', () => { + const response = getSearchListItemMock(); + response.hits.hits[0]._source.ip = '127.0.0.1'; + response.hits.hits[0]._source.keyword = undefined; + expect(() => + transformElasticToListItem({ + response, + type: 'keyword', + }) + ).toThrow('Was expecting keyword to not be null/undefined'); + }); +}); diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts index 9cf673081d3206..2dc0f4fe7a8216 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts @@ -9,13 +9,15 @@ import { SearchResponse } from 'elasticsearch'; import { ListItemArraySchema, SearchEsListItemSchema, Type } from '../../../common/schemas'; import { ErrorWithStatusCode } from '../../error_with_status_code'; +export interface TransformElasticToListItemOptions { + response: SearchResponse; + type: Type; +} + export const transformElasticToListItem = ({ response, type, -}: { - response: SearchResponse; - type: Type; -}): ListItemArraySchema => { +}: TransformElasticToListItemOptions): ListItemArraySchema => { return response.hits.hits.map(hit => { const { _id, @@ -64,11 +66,10 @@ export const transformElasticToListItem = ({ }; } } - // TypeScript is not happy unless I have this line here return assertUnreachable(); }); }; -export const assertUnreachable = (): never => { +const assertUnreachable = (): never => { throw new Error('Unknown type in elastic_to_list_items'); }; diff --git a/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.test.ts b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.test.ts new file mode 100644 index 00000000000000..217cad30bfdbbd --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EsDataTypeUnion, Type } from '../../../common/schemas'; + +import { transformListItemToElasticQuery } from './transform_list_item_to_elastic_query'; + +describe('transform_elastic_to_elastic_query', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it transforms a ip type and value to a union', () => { + const elasticQuery = transformListItemToElasticQuery({ + type: 'ip', + value: '127.0.0.1', + }); + const expected: EsDataTypeUnion = { ip: '127.0.0.1' }; + expect(elasticQuery).toEqual(expected); + }); + + test('it transforms a keyword type and value to a union', () => { + const elasticQuery = transformListItemToElasticQuery({ + type: 'keyword', + value: 'host-name', + }); + const expected: EsDataTypeUnion = { keyword: 'host-name' }; + expect(elasticQuery).toEqual(expected); + }); + + test('it throws if the type is not known', () => { + const type: Type = 'made-up' as Type; + expect(() => + transformListItemToElasticQuery({ + type, + value: 'some-value', + }) + ).toThrow('Unknown type: "made-up" in transformListItemToElasticQuery'); + }); +}); diff --git a/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts index e68851dc3582b7..051802cc41b5ba 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts @@ -12,8 +12,6 @@ export const transformListItemToElasticQuery = ({ }: { type: Type; value: string; - // We disable the consistent return since we want to use typescript for exhaustive type checks - // eslint-disable-next-line consistent-return }): EsDataTypeUnion => { switch (type) { case 'ip': { @@ -27,4 +25,9 @@ export const transformListItemToElasticQuery = ({ }; } } + return assertUnreachable(type); +}; + +const assertUnreachable = (type: string): never => { + throw new Error(`Unknown type: "${type}" in transformListItemToElasticQuery`); }; diff --git a/x-pack/plugins/lists/server/types.ts b/x-pack/plugins/lists/server/types.ts index 7d509c4e271675..e0e4495d47c341 100644 --- a/x-pack/plugins/lists/server/types.ts +++ b/x-pack/plugins/lists/server/types.ts @@ -4,18 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IContextProvider, RequestHandler, ScopedClusterClient } from 'kibana/server'; +import { IContextProvider, RequestHandler } from 'kibana/server'; import { SecurityPluginSetup } from '../../security/server'; import { SpacesPluginSetup } from '../../spaces/server'; import { ListClient } from './services/lists/client'; -export type DataClient = Pick; export type ContextProvider = IContextProvider, 'lists'>; export interface PluginsSetup { - security: SecurityPluginSetup; + security: SecurityPluginSetup | undefined | null; spaces: SpacesPluginSetup | undefined | null; } From 4e58d7ae0a54023e9fbdebc5ede1252e2ee71164 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Wed, 29 Apr 2020 22:03:41 -0400 Subject: [PATCH 18/30] [Lens] Use a size of 5 for first string field in visualization (#64726) --- .../dimension_panel/dimension_panel.test.tsx | 38 +++++++++++++++++++ .../indexpattern_suggestions.test.tsx | 5 ++- .../indexpattern_suggestions.ts | 8 ++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 074c40759f8d82..9df79aa9e09085 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -1380,5 +1380,43 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }); }); + + it('does not set the size of the terms aggregation', () => { + const dragging = { + field: { type: 'string', name: 'mystring', aggregatable: true }, + indexPatternId: 'foo', + }; + const testState = dragDropState(); + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.isBucketed, + layerId: 'myLayer', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + myLayer: { + ...testState.layers.myLayer, + columnOrder: ['col1', 'col2'], + columns: { + ...testState.layers.myLayer.columns, + col2: expect.objectContaining({ + operationType: 'terms', + params: expect.objectContaining({ size: 3 }), + }), + }, + }, + }, + }); + }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index 2008b326a539ce..02471b935c97c0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -184,6 +184,7 @@ describe('IndexPattern Data Source suggestions', () => { id2: expect.objectContaining({ operationType: 'terms', sourceField: 'source', + params: expect.objectContaining({ size: 5 }), }), id3: expect.objectContaining({ operationType: 'count', @@ -388,6 +389,7 @@ describe('IndexPattern Data Source suggestions', () => { id1: expect.objectContaining({ operationType: 'terms', sourceField: 'source', + params: expect.objectContaining({ size: 5 }), }), id2: expect.objectContaining({ operationType: 'count', @@ -779,7 +781,7 @@ describe('IndexPattern Data Source suggestions', () => { expect(suggestions[0].table.columns[0].operation.isBucketed).toBeFalsy(); }); - it('appends a terms column on string field', () => { + it('appends a terms column with default size on string field', () => { const initialState = stateWithNonEmptyTables(); const suggestions = getDatasourceSuggestionsForField(initialState, '1', { name: 'dest', @@ -800,6 +802,7 @@ describe('IndexPattern Data Source suggestions', () => { id1: expect.objectContaining({ operationType: 'terms', sourceField: 'dest', + params: expect.objectContaining({ size: 3 }), }), }, }), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index 2b3e976a77ea75..44963722f8afcb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -17,6 +17,7 @@ import { OperationType, } from './operations'; import { operationDefinitions } from './operations/definitions'; +import { TermsIndexPatternColumn } from './operations/definitions/terms'; import { hasField } from './utils'; import { IndexPattern, @@ -232,6 +233,10 @@ function addFieldAsBucketOperation( [newColumnId]: newColumn, }; + if (buckets.length === 0 && operation === 'terms') { + (newColumn as TermsIndexPatternColumn).params.size = 5; + } + const oldDateHistogramIndex = layer.columnOrder.findIndex( columnId => layer.columns[columnId].operationType === 'date_histogram' ); @@ -327,6 +332,9 @@ function createNewLayerWithBucketAggregation( field, suggestedPriority: undefined, }); + if (operation === 'terms') { + (column as TermsIndexPatternColumn).params.size = 5; + } return { indexPatternId: indexPattern.id, From f85b3898f618d7f62178eb0772e4c2aee0f24335 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Thu, 30 Apr 2020 00:27:51 -0400 Subject: [PATCH 19/30] [Event Log] add rel=primary to saved objects for query targets (#64615) resolves https://github.com/elastic/kibana/issues/62668 Adds a property named `rel` to the nested saved objects in the event documents, whose value should not be set, or set to `primary`. The query by saved object function changes to only match event documents with that saved objects if it has the `rel: primary` value. This is used to limit searching alerting's executeAction event document with only the alert saved object, and not the action saved object (this document has an alert and action saved object). The alert saved object has the `rel: primary` field set, and the action does not. Previously, those documents were returned with a query of the action saved object. --- .../actions/server/lib/action_executor.ts | 13 +++++++-- .../create_execution_handler.test.ts | 1 + .../task_runner/create_execution_handler.ts | 4 +-- .../server/task_runner/task_runner.test.ts | 7 +++++ .../server/task_runner/task_runner.ts | 22 ++++++++++++-- .../plugins/event_log/generated/mappings.json | 4 +++ x-pack/plugins/event_log/generated/schemas.ts | 1 + x-pack/plugins/event_log/scripts/mappings.js | 6 ++++ .../server/es/cluster_client_adapter.test.ts | 21 ++++++++++++++ .../server/es/cluster_client_adapter.ts | 9 +++++- .../event_log/server/event_logger.test.ts | 29 +++++++++++++++++++ .../plugins/event_log/server/event_logger.ts | 15 +++++++++- x-pack/plugins/event_log/server/index.ts | 8 ++++- x-pack/plugins/event_log/server/types.ts | 2 ++ .../plugins/event_log/server/init_routes.ts | 2 +- .../event_log/public_api_integration.ts | 1 + .../event_log/service_api_integration.ts | 2 +- 17 files changed, 135 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index 101e18f2583e39..3e9262c05efac4 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -17,7 +17,7 @@ import { import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; import { SpacesServiceSetup } from '../../../spaces/server'; import { EVENT_LOG_ACTIONS } from '../plugin'; -import { IEvent, IEventLogger } from '../../../event_log/server'; +import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server'; export interface ActionExecutorContext { logger: Logger; @@ -110,7 +110,16 @@ export class ActionExecutor { const actionLabel = `${actionTypeId}:${actionId}: ${name}`; const event: IEvent = { event: { action: EVENT_LOG_ACTIONS.execute }, - kibana: { saved_objects: [{ type: 'action', id: actionId, ...namespace }] }, + kibana: { + saved_objects: [ + { + rel: SAVED_OBJECT_REL_PRIMARY, + type: 'action', + id: actionId, + ...namespace, + }, + ], + }, }; eventLogger.startTiming(event); diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts index 0e46ef4919626a..a564b87f2ca50c 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts @@ -95,6 +95,7 @@ test('calls actionsPlugin.execute per selected action', async () => { "saved_objects": Array [ Object { "id": "1", + "rel": "primary", "type": "alert", }, Object { diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts index 5c3e36b88879dd..16fadc8b06cd59 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts @@ -9,7 +9,7 @@ import { AlertAction, State, Context, AlertType } from '../types'; import { Logger } from '../../../../../src/core/server'; import { transformActionParams } from './transform_action_params'; import { PluginStartContract as ActionsPluginStartContract } from '../../../../plugins/actions/server'; -import { IEventLogger, IEvent } from '../../../event_log/server'; +import { IEventLogger, IEvent, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server'; import { EVENT_LOG_ACTIONS } from '../plugin'; interface CreateExecutionHandlerOptions { @@ -96,7 +96,7 @@ export function createExecutionHandler({ instance_id: alertInstanceId, }, saved_objects: [ - { type: 'alert', id: alertId, ...namespace }, + { rel: SAVED_OBJECT_REL_PRIMARY, type: 'alert', id: alertId, ...namespace }, { type: 'action', id: action.id, ...namespace }, ], }, diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 26d8a1d1777c04..35a0018049c335 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -172,6 +172,7 @@ describe('Task Runner', () => { Object { "id": "1", "namespace": undefined, + "rel": "primary", "type": "alert", }, ], @@ -234,6 +235,7 @@ describe('Task Runner', () => { Object { "id": "1", "namespace": undefined, + "rel": "primary", "type": "alert", }, ], @@ -254,6 +256,7 @@ describe('Task Runner', () => { Object { "id": "1", "namespace": undefined, + "rel": "primary", "type": "alert", }, ], @@ -274,6 +277,7 @@ describe('Task Runner', () => { Object { "id": "1", "namespace": undefined, + "rel": "primary", "type": "alert", }, Object { @@ -351,6 +355,7 @@ describe('Task Runner', () => { Object { "id": "1", "namespace": undefined, + "rel": "primary", "type": "alert", }, ], @@ -371,6 +376,7 @@ describe('Task Runner', () => { Object { "id": "1", "namespace": undefined, + "rel": "primary", "type": "alert", }, ], @@ -568,6 +574,7 @@ describe('Task Runner', () => { Object { "id": "1", "namespace": undefined, + "rel": "primary", "type": "alert", }, ], diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 26970dc6b2b0d9..bf005301adc07c 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -25,7 +25,7 @@ import { promiseResult, map, Resultable, asOk, asErr, resolveErr } from '../lib/ import { taskInstanceToAlertTaskInstance } from './alert_task_instance'; import { AlertInstances } from '../alert_instance/alert_instance'; import { EVENT_LOG_ACTIONS } from '../plugin'; -import { IEvent, IEventLogger } from '../../../event_log/server'; +import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server'; import { isAlertSavedObjectNotFoundError } from '../lib/is_alert_not_found_error'; const FALLBACK_RETRY_INTERVAL: IntervalSchedule = { interval: '5m' }; @@ -174,7 +174,16 @@ export class TaskRunner { const alertLabel = `${this.alertType.id}:${alertId}: '${name}'`; const event: IEvent = { event: { action: EVENT_LOG_ACTIONS.execute }, - kibana: { saved_objects: [{ type: 'alert', id: alertId, namespace }] }, + kibana: { + saved_objects: [ + { + rel: SAVED_OBJECT_REL_PRIMARY, + type: 'alert', + id: alertId, + namespace, + }, + ], + }, }; eventLogger.startTiming(event); @@ -393,7 +402,14 @@ function generateNewAndResolvedInstanceEvents(params: GenerateNewAndResolvedInst alerting: { instance_id: id, }, - saved_objects: [{ type: 'alert', id: params.alertId, namespace: params.namespace }], + saved_objects: [ + { + rel: SAVED_OBJECT_REL_PRIMARY, + type: 'alert', + id: params.alertId, + namespace: params.namespace, + }, + ], }, message, }; diff --git a/x-pack/plugins/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json index f487e9262e50e7..0a858969c4f6af 100644 --- a/x-pack/plugins/event_log/generated/mappings.json +++ b/x-pack/plugins/event_log/generated/mappings.json @@ -86,6 +86,10 @@ }, "saved_objects": { "properties": { + "rel": { + "type": "keyword", + "ignore_above": 1024 + }, "namespace": { "type": "keyword", "ignore_above": 1024 diff --git a/x-pack/plugins/event_log/generated/schemas.ts b/x-pack/plugins/event_log/generated/schemas.ts index 9c923fe77d0358..57fe90a8e876ed 100644 --- a/x-pack/plugins/event_log/generated/schemas.ts +++ b/x-pack/plugins/event_log/generated/schemas.ts @@ -65,6 +65,7 @@ export const EventSchema = schema.maybe( saved_objects: schema.maybe( schema.arrayOf( schema.object({ + rel: ecsString(), namespace: ecsString(), id: ecsString(), type: ecsString(), diff --git a/x-pack/plugins/event_log/scripts/mappings.js b/x-pack/plugins/event_log/scripts/mappings.js index 8cc2c74b60e57b..fd149d132031e1 100644 --- a/x-pack/plugins/event_log/scripts/mappings.js +++ b/x-pack/plugins/event_log/scripts/mappings.js @@ -24,6 +24,11 @@ exports.EcsKibanaExtensionsMappings = { saved_objects: { type: 'nested', properties: { + // relation; currently only supports "primary" or not set + rel: { + type: 'keyword', + ignore_above: 1024, + }, // relevant kibana space namespace: { type: 'keyword', @@ -58,6 +63,7 @@ exports.EcsEventLogProperties = [ 'user.name', 'kibana.server_uuid', 'kibana.alerting.instance_id', + 'kibana.saved_objects.rel', 'kibana.saved_objects.namespace', 'kibana.saved_objects.id', 'kibana.saved_objects.name', diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts index f79962a3241317..66c16d0ddf3838 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts @@ -236,6 +236,13 @@ describe('queryEventsBySavedObject', () => { query: { bool: { must: [ + { + term: { + 'kibana.saved_objects.rel': { + value: 'primary', + }, + }, + }, { term: { 'kibana.saved_objects.type': { @@ -319,6 +326,13 @@ describe('queryEventsBySavedObject', () => { query: { bool: { must: [ + { + term: { + 'kibana.saved_objects.rel': { + value: 'primary', + }, + }, + }, { term: { 'kibana.saved_objects.type': { @@ -388,6 +402,13 @@ describe('queryEventsBySavedObject', () => { query: { bool: { must: [ + { + term: { + 'kibana.saved_objects.rel': { + value: 'primary', + }, + }, + }, { term: { 'kibana.saved_objects.type': { diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts index 47d273b9981e32..c0ff87234c09db 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts @@ -7,7 +7,7 @@ import { reject, isUndefined } from 'lodash'; import { SearchResponse, Client } from 'elasticsearch'; import { Logger, ClusterClient } from '../../../../../src/core/server'; -import { IEvent } from '../types'; +import { IEvent, SAVED_OBJECT_REL_PRIMARY } from '../types'; import { FindOptionsType } from '../event_log_client'; export type EsClusterClient = Pick; @@ -155,6 +155,13 @@ export class ClusterClientAdapter { query: { bool: { must: [ + { + term: { + 'kibana.saved_objects.rel': { + value: SAVED_OBJECT_REL_PRIMARY, + }, + }, + }, { term: { 'kibana.saved_objects.type': { diff --git a/x-pack/plugins/event_log/server/event_logger.test.ts b/x-pack/plugins/event_log/server/event_logger.test.ts index 6a745931420c0c..2bda194a65d133 100644 --- a/x-pack/plugins/event_log/server/event_logger.test.ts +++ b/x-pack/plugins/event_log/server/event_logger.test.ts @@ -150,6 +150,35 @@ describe('EventLogger', () => { message = await waitForLogMessage(systemLogger); expect(message).toMatch(/invalid event logged.*action.*undefined.*/); }); + + test('logs warnings when writing invalid events', async () => { + service.registerProviderActions('provider', ['action-a']); + eventLogger = service.getLogger({}); + + eventLogger.logEvent(({ event: { PROVIDER: 'provider' } } as unknown) as IEvent); + let message = await waitForLogMessage(systemLogger); + expect(message).toMatch(/invalid event logged.*provider.*undefined.*/); + + const event: IEvent = { + event: { + provider: 'provider', + action: 'action-a', + }, + kibana: { + saved_objects: [ + { + rel: 'ZZZ-primary', + namespace: 'default', + type: 'event_log_test', + id: '123', + }, + ], + }, + }; + eventLogger.logEvent(event); + message = await waitForLogMessage(systemLogger); + expect(message).toMatch(/invalid rel property.*ZZZ-primary.*/); + }); }); // return the next logged event; throw if not an event diff --git a/x-pack/plugins/event_log/server/event_logger.ts b/x-pack/plugins/event_log/server/event_logger.ts index bcfd7bd45a6f5c..1a710a6fa48653 100644 --- a/x-pack/plugins/event_log/server/event_logger.ts +++ b/x-pack/plugins/event_log/server/event_logger.ts @@ -19,6 +19,7 @@ import { ECS_VERSION, EventSchema, } from './types'; +import { SAVED_OBJECT_REL_PRIMARY } from './types'; type SystemLogger = Plugin['systemLogger']; @@ -118,6 +119,8 @@ const RequiredEventSchema = schema.object({ action: schema.string({ minLength: 1 }), }); +const ValidSavedObjectRels = new Set([undefined, SAVED_OBJECT_REL_PRIMARY]); + function validateEvent(eventLogService: IEventLogService, event: IEvent): IValidatedEvent { if (event?.event == null) { throw new Error(`no "event" property`); @@ -137,7 +140,17 @@ function validateEvent(eventLogService: IEventLogService, event: IEvent): IValid } // could throw an error - return EventSchema.validate(event); + const result = EventSchema.validate(event); + + if (result?.kibana?.saved_objects?.length) { + for (const so of result?.kibana?.saved_objects) { + if (!ValidSavedObjectRels.has(so.rel)) { + throw new Error(`invalid rel property in saved_objects: "${so.rel}"`); + } + } + } + + return result; } export const EVENT_LOGGED_PREFIX = `event logged: `; diff --git a/x-pack/plugins/event_log/server/index.ts b/x-pack/plugins/event_log/server/index.ts index b7fa25cb6eb9cc..0612b5319c15b2 100644 --- a/x-pack/plugins/event_log/server/index.ts +++ b/x-pack/plugins/event_log/server/index.ts @@ -8,6 +8,12 @@ import { PluginInitializerContext } from 'src/core/server'; import { ConfigSchema } from './types'; import { Plugin } from './plugin'; -export { IEventLogService, IEventLogger, IEventLogClientService, IEvent } from './types'; +export { + IEventLogService, + IEventLogger, + IEventLogClientService, + IEvent, + SAVED_OBJECT_REL_PRIMARY, +} from './types'; export const config = { schema: ConfigSchema }; export const plugin = (context: PluginInitializerContext) => new Plugin(context); diff --git a/x-pack/plugins/event_log/server/types.ts b/x-pack/plugins/event_log/server/types.ts index baf53ef4479141..58be6707b03730 100644 --- a/x-pack/plugins/event_log/server/types.ts +++ b/x-pack/plugins/event_log/server/types.ts @@ -13,6 +13,8 @@ import { IEvent } from '../generated/schemas'; import { FindOptionsType } from './event_log_client'; import { QueryEventsBySavedObjectResult } from './es/cluster_client_adapter'; +export const SAVED_OBJECT_REL_PRIMARY = 'primary'; + export const ConfigSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), logEntries: schema.boolean({ defaultValue: false }), diff --git a/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts b/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts index c5f3e65581df97..9622715e87e55f 100644 --- a/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts +++ b/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts @@ -40,7 +40,7 @@ export const logEventRoute = (router: IRouter, eventLogger: IEventLogger, logger } catch (ex) { logger.info(`log event error: ${ex}`); await context.core.savedObjects.client.create('event_log_test', {}, { id }); - logger.info(`created saved object`); + logger.info(`created saved object ${id}`); } eventLogger.logEvent(event); logger.info(`logged`); diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts index d7bbc29bd861e2..f3a3d58336b1db 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts @@ -205,6 +205,7 @@ export default function({ getService }: FtrProviderContext) { kibana: { saved_objects: [ { + rel: 'primary', namespace: 'default', type: 'event_log_test', id, diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts index 31668e8345275e..361d80aaedd419 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts @@ -101,7 +101,7 @@ export default function({ getService }: FtrProviderContext) { const eventId = uuid.v4(); const event: IEvent = { event: { action: 'action1', provider: 'provider4' }, - kibana: { saved_objects: [{ type: 'event_log_test', id: eventId }] }, + kibana: { saved_objects: [{ rel: 'primary', type: 'event_log_test', id: eventId }] }, }; await logTestEvent(eventId, event); From aa8cb620bb63afbb631e81b7e5a507e8075a1b74 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 30 Apr 2020 08:05:16 +0200 Subject: [PATCH 20/30] Allow to define and update a defaultPath for applications (#64498) * add defaultPath to `AppBase` and use it in `navigateToApp` * add removeSlashes util * adapt `toNavLink` to handle defaultPath * update generated doc * codestyle * add FTR test * address comments * add tests --- ...-plugin-core-public.appbase.defaultpath.md | 13 +++ .../kibana-plugin-core-public.appbase.md | 1 + ...a-plugin-core-public.appupdatablefields.md | 2 +- ...kibana-plugin-core-public.chromenavlink.md | 2 +- ...na-plugin-core-public.chromenavlink.url.md | 6 +- .../application/application_service.test.ts | 87 ++++++++++++++++++- .../application/application_service.tsx | 12 ++- src/core/public/application/types.ts | 15 +++- src/core/public/application/utils.test.ts | 71 +++++++++++++++ src/core/public/application/utils.ts | 54 ++++++++++++ src/core/public/chrome/nav_links/nav_link.ts | 18 ++-- .../chrome/nav_links/to_nav_link.test.ts | 32 +++++++ .../public/chrome/nav_links/to_nav_link.ts | 11 ++- src/core/public/chrome/ui/header/nav_link.tsx | 2 +- src/core/public/public.api.md | 4 +- .../core_plugins/application_status.ts | 26 ++++++ 16 files changed, 320 insertions(+), 36 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.appbase.defaultpath.md create mode 100644 src/core/public/application/utils.test.ts create mode 100644 src/core/public/application/utils.ts diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.defaultpath.md b/docs/development/core/public/kibana-plugin-core-public.appbase.defaultpath.md new file mode 100644 index 00000000000000..51492756ef2327 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.appbase.defaultpath.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppBase](./kibana-plugin-core-public.appbase.md) > [defaultPath](./kibana-plugin-core-public.appbase.defaultpath.md) + +## AppBase.defaultPath property + +Allow to define the default path a user should be directed to when navigating to the app. When defined, this value will be used as a default for the `path` option when calling [navigateToApp](./kibana-plugin-core-public.applicationstart.navigatetoapp.md)\`, and will also be appended to the [application navLink](./kibana-plugin-core-public.chromenavlink.md) in the navigation bar. + +Signature: + +```typescript +defaultPath?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.md b/docs/development/core/public/kibana-plugin-core-public.appbase.md index b73785647f23cc..7b624f12ac1df0 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.md +++ b/docs/development/core/public/kibana-plugin-core-public.appbase.md @@ -18,6 +18,7 @@ export interface AppBase | [capabilities](./kibana-plugin-core-public.appbase.capabilities.md) | Partial<Capabilities> | Custom capabilities defined by the app. | | [category](./kibana-plugin-core-public.appbase.category.md) | AppCategory | The category definition of the product See [AppCategory](./kibana-plugin-core-public.appcategory.md) See DEFAULT\_APP\_CATEGORIES for more reference | | [chromeless](./kibana-plugin-core-public.appbase.chromeless.md) | boolean | Hide the UI chrome when the application is mounted. Defaults to false. Takes precedence over chrome service visibility settings. | +| [defaultPath](./kibana-plugin-core-public.appbase.defaultpath.md) | string | Allow to define the default path a user should be directed to when navigating to the app. When defined, this value will be used as a default for the path option when calling [navigateToApp](./kibana-plugin-core-public.applicationstart.navigatetoapp.md)\`, and will also be appended to the [application navLink](./kibana-plugin-core-public.chromenavlink.md) in the navigation bar. | | [euiIconType](./kibana-plugin-core-public.appbase.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precendence over the icon property. | | [icon](./kibana-plugin-core-public.appbase.icon.md) | string | A URL to an image file used as an icon. Used as a fallback if euiIconType is not provided. | | [id](./kibana-plugin-core-public.appbase.id.md) | string | The unique identifier of the application | diff --git a/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md b/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md index cdf9171a46aed0..3d8b5d115c8a27 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md +++ b/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md @@ -9,5 +9,5 @@ Defines the list of fields that can be updated via an [AppUpdater](./kibana-plug Signature: ```typescript -export declare type AppUpdatableFields = Pick; +export declare type AppUpdatableFields = Pick; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md index 1cc1a1194a5379..a9fabb38df8696 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md @@ -29,5 +29,5 @@ export interface ChromeNavLink | [subUrlBase](./kibana-plugin-core-public.chromenavlink.suburlbase.md) | string | A url base that legacy apps can set to match deep URLs to an application. | | [title](./kibana-plugin-core-public.chromenavlink.title.md) | string | The title of the application. | | [tooltip](./kibana-plugin-core-public.chromenavlink.tooltip.md) | string | A tooltip shown when hovering over an app link. | -| [url](./kibana-plugin-core-public.chromenavlink.url.md) | string | A url that legacy apps can set to deep link into their applications. | +| [url](./kibana-plugin-core-public.chromenavlink.url.md) | string | The route used to open the [default path](./kibana-plugin-core-public.appbase.defaultpath.md) of an application. If unset, baseUrl will be used instead. | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.url.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.url.md index 0c415ed1a7fadc..1e0b8900159930 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.url.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.url.md @@ -4,11 +4,7 @@ ## ChromeNavLink.url property -> Warning: This API is now obsolete. -> -> - -A url that legacy apps can set to deep link into their applications. +The route used to open the [default path](./kibana-plugin-core-public.appbase.defaultpath.md) of an application. If unset, `baseUrl` will be used instead. Signature: diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index c25918c6b7328c..e29837aecb1253 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -87,7 +87,7 @@ describe('#setup()', () => { ).toThrowErrorMatchingInlineSnapshot(`"Applications cannot be registered after \\"setup\\""`); }); - it('allows to register a statusUpdater for the application', async () => { + it('allows to register an AppUpdater for the application', async () => { const setup = service.setup(setupDeps); const pluginId = Symbol('plugin'); @@ -118,6 +118,7 @@ describe('#setup()', () => { updater$.next(app => ({ status: AppStatus.inaccessible, tooltip: 'App inaccessible due to reason', + defaultPath: 'foo/bar', })); applications = await applications$.pipe(take(1)).toPromise(); @@ -128,6 +129,7 @@ describe('#setup()', () => { legacy: false, navLinkStatus: AppNavLinkStatus.default, status: AppStatus.inaccessible, + defaultPath: 'foo/bar', tooltip: 'App inaccessible due to reason', }) ); @@ -209,7 +211,7 @@ describe('#setup()', () => { }); }); - describe('registerAppStatusUpdater', () => { + describe('registerAppUpdater', () => { it('updates status fields', async () => { const setup = service.setup(setupDeps); @@ -413,6 +415,36 @@ describe('#setup()', () => { }) ); }); + + it('allows to update the basePath', async () => { + const setup = service.setup(setupDeps); + + const pluginId = Symbol('plugin'); + setup.register(pluginId, createApp({ id: 'app1' })); + + const updater = new BehaviorSubject(app => ({})); + setup.registerAppUpdater(updater); + + const start = await service.start(startDeps); + await start.navigateToApp('app1'); + expect(MockHistory.push).toHaveBeenCalledWith('/app/app1', undefined); + MockHistory.push.mockClear(); + + updater.next(app => ({ defaultPath: 'default-path' })); + await start.navigateToApp('app1'); + expect(MockHistory.push).toHaveBeenCalledWith('/app/app1/default-path', undefined); + MockHistory.push.mockClear(); + + updater.next(app => ({ defaultPath: 'another-path' })); + await start.navigateToApp('app1'); + expect(MockHistory.push).toHaveBeenCalledWith('/app/app1/another-path', undefined); + MockHistory.push.mockClear(); + + updater.next(app => ({})); + await start.navigateToApp('app1'); + expect(MockHistory.push).toHaveBeenCalledWith('/app/app1', undefined); + MockHistory.push.mockClear(); + }); }); it("`registerMountContext` calls context container's registerContext", () => { @@ -676,6 +708,57 @@ describe('#start()', () => { expect(MockHistory.push).toHaveBeenCalledWith('/custom/path#/hash/router/path', undefined); }); + it('preserves trailing slash when path contains a hash', async () => { + const { register } = service.setup(setupDeps); + + register(Symbol(), createApp({ id: 'app2', appRoute: '/custom/app-path' })); + + const { navigateToApp } = await service.start(startDeps); + await navigateToApp('app2', { path: '#/' }); + expect(MockHistory.push).toHaveBeenCalledWith('/custom/app-path#/', undefined); + MockHistory.push.mockClear(); + + await navigateToApp('app2', { path: '#/foo/bar/' }); + expect(MockHistory.push).toHaveBeenCalledWith('/custom/app-path#/foo/bar/', undefined); + MockHistory.push.mockClear(); + + await navigateToApp('app2', { path: '/path#/' }); + expect(MockHistory.push).toHaveBeenCalledWith('/custom/app-path/path#/', undefined); + MockHistory.push.mockClear(); + + await navigateToApp('app2', { path: '/path#/hash/' }); + expect(MockHistory.push).toHaveBeenCalledWith('/custom/app-path/path#/hash/', undefined); + MockHistory.push.mockClear(); + + await navigateToApp('app2', { path: '/path/' }); + expect(MockHistory.push).toHaveBeenCalledWith('/custom/app-path/path', undefined); + MockHistory.push.mockClear(); + }); + + it('appends the defaultPath when the path parameter is not specified', async () => { + const { register } = service.setup(setupDeps); + + register(Symbol(), createApp({ id: 'app1', defaultPath: 'default/path' })); + register( + Symbol(), + createApp({ id: 'app2', appRoute: '/custom-app-path', defaultPath: '/my-base' }) + ); + + const { navigateToApp } = await service.start(startDeps); + + await navigateToApp('app1', { path: 'defined-path' }); + expect(MockHistory.push).toHaveBeenCalledWith('/app/app1/defined-path', undefined); + + await navigateToApp('app1', {}); + expect(MockHistory.push).toHaveBeenCalledWith('/app/app1/default/path', undefined); + + await navigateToApp('app2', { path: 'defined-path' }); + expect(MockHistory.push).toHaveBeenCalledWith('/custom-app-path/defined-path', undefined); + + await navigateToApp('app2', {}); + expect(MockHistory.push).toHaveBeenCalledWith('/custom-app-path/my-base', undefined); + }); + it('includes state if specified', async () => { const { register } = service.setup(setupDeps); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 1c9492d81c7f68..bafa1932e5e927 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -46,6 +46,7 @@ import { Mounter, } from './types'; import { getLeaveAction, isConfirmAction } from './application_leave'; +import { appendAppPath } from './utils'; interface SetupDeps { context: ContextSetup; @@ -81,13 +82,7 @@ const getAppUrl = (mounters: Map, appId: string, path: string = const appBasePath = mounters.get(appId)?.appRoute ? `/${mounters.get(appId)!.appRoute}` : `/app/${appId}`; - - // Only preppend slash if not a hash or query path - path = path.startsWith('#') || path.startsWith('?') ? path : `/${path}`; - - return `${appBasePath}${path}` - .replace(/\/{2,}/g, '/') // Remove duplicate slashes - .replace(/\/$/, ''); // Remove trailing slash + return appendAppPath(appBasePath, path); }; const allApplicationsFilter = '__ALL__'; @@ -290,6 +285,9 @@ export class ApplicationService { }, navigateToApp: async (appId, { path, state }: { path?: string; state?: any } = {}) => { if (await this.shouldNavigate(overlays)) { + if (path === undefined) { + path = applications$.value.get(appId)?.defaultPath; + } this.appLeaveHandlers.delete(this.currentAppId$.value!); this.navigate!(getAppUrl(availableMounters, appId, path), state); this.currentAppId$.next(appId); diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 318afb652999ef..0734e178033e24 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -66,6 +66,13 @@ export interface AppBase { */ navLinkStatus?: AppNavLinkStatus; + /** + * Allow to define the default path a user should be directed to when navigating to the app. + * When defined, this value will be used as a default for the `path` option when calling {@link ApplicationStart.navigateToApp | navigateToApp}`, + * and will also be appended to the {@link ChromeNavLink | application navLink} in the navigation bar. + */ + defaultPath?: string; + /** * An {@link AppUpdater} observable that can be used to update the application {@link AppUpdatableFields} at runtime. * @@ -187,7 +194,10 @@ export enum AppNavLinkStatus { * Defines the list of fields that can be updated via an {@link AppUpdater}. * @public */ -export type AppUpdatableFields = Pick; +export type AppUpdatableFields = Pick< + AppBase, + 'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath' +>; /** * Updater for applications. @@ -642,7 +652,8 @@ export interface ApplicationStart { * Navigate to a given app * * @param appId - * @param options.path - optional path inside application to deep link to + * @param options.path - optional path inside application to deep link to. + * If undefined, will use {@link AppBase.defaultPath | the app's default path}` as default. * @param options.state - optional state to forward to the application */ navigateToApp(appId: string, options?: { path?: string; state?: any }): Promise; diff --git a/src/core/public/application/utils.test.ts b/src/core/public/application/utils.test.ts new file mode 100644 index 00000000000000..7ed0919f88c614 --- /dev/null +++ b/src/core/public/application/utils.test.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { removeSlashes, appendAppPath } from './utils'; + +describe('removeSlashes', () => { + it('only removes duplicates by default', () => { + expect(removeSlashes('/some//url//to//')).toEqual('/some/url/to/'); + expect(removeSlashes('some/////other//url')).toEqual('some/other/url'); + }); + + it('remove trailing slash when `trailing` is true', () => { + expect(removeSlashes('/some//url//to//', { trailing: true })).toEqual('/some/url/to'); + }); + + it('remove leading slash when `leading` is true', () => { + expect(removeSlashes('/some//url//to//', { leading: true })).toEqual('some/url/to/'); + }); + + it('does not removes duplicates when `duplicates` is false', () => { + expect(removeSlashes('/some//url//to/', { leading: true, duplicates: false })).toEqual( + 'some//url//to/' + ); + expect(removeSlashes('/some//url//to/', { trailing: true, duplicates: false })).toEqual( + '/some//url//to' + ); + }); + + it('accept mixed options', () => { + expect( + removeSlashes('/some//url//to/', { leading: true, duplicates: false, trailing: true }) + ).toEqual('some//url//to'); + expect( + removeSlashes('/some//url//to/', { leading: true, duplicates: true, trailing: true }) + ).toEqual('some/url/to'); + }); +}); + +describe('appendAppPath', () => { + it('appends the appBasePath with given path', () => { + expect(appendAppPath('/app/my-app', '/some-path')).toEqual('/app/my-app/some-path'); + expect(appendAppPath('/app/my-app/', 'some-path')).toEqual('/app/my-app/some-path'); + expect(appendAppPath('/app/my-app', 'some-path')).toEqual('/app/my-app/some-path'); + expect(appendAppPath('/app/my-app', '')).toEqual('/app/my-app'); + }); + + it('preserves the trailing slash only if included in the hash', () => { + expect(appendAppPath('/app/my-app', '/some-path/')).toEqual('/app/my-app/some-path'); + expect(appendAppPath('/app/my-app', '/some-path#/')).toEqual('/app/my-app/some-path#/'); + expect(appendAppPath('/app/my-app', '/some-path#/hash/')).toEqual( + '/app/my-app/some-path#/hash/' + ); + expect(appendAppPath('/app/my-app', '/some-path#/hash')).toEqual('/app/my-app/some-path#/hash'); + }); +}); diff --git a/src/core/public/application/utils.ts b/src/core/public/application/utils.ts new file mode 100644 index 00000000000000..048f195fe12230 --- /dev/null +++ b/src/core/public/application/utils.ts @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Utility to remove trailing, leading or duplicate slashes. + * By default will only remove duplicates. + */ +export const removeSlashes = ( + url: string, + { + trailing = false, + leading = false, + duplicates = true, + }: { trailing?: boolean; leading?: boolean; duplicates?: boolean } = {} +): string => { + if (duplicates) { + url = url.replace(/\/{2,}/g, '/'); + } + if (trailing) { + url = url.replace(/\/$/, ''); + } + if (leading) { + url = url.replace(/^\//, ''); + } + return url; +}; + +export const appendAppPath = (appBasePath: string, path: string = '') => { + // Only prepend slash if not a hash or query path + path = path === '' || path.startsWith('#') || path.startsWith('?') ? path : `/${path}`; + // Do not remove trailing slash when in hashbang + const removeTrailing = path.indexOf('#') === -1; + return removeSlashes(`${appBasePath}${path}`, { + trailing: removeTrailing, + duplicates: true, + leading: false, + }); +}; diff --git a/src/core/public/chrome/nav_links/nav_link.ts b/src/core/public/chrome/nav_links/nav_link.ts index d0ef2aeb265feb..fb2972735c2b70 100644 --- a/src/core/public/chrome/nav_links/nav_link.ts +++ b/src/core/public/chrome/nav_links/nav_link.ts @@ -44,6 +44,12 @@ export interface ChromeNavLink { */ readonly baseUrl: string; + /** + * The route used to open the {@link AppBase.defaultPath | default path } of an application. + * If unset, `baseUrl` will be used instead. + */ + readonly url?: string; + /** * An ordinal used to sort nav links relative to one another for display. */ @@ -99,18 +105,6 @@ export interface ChromeNavLink { */ readonly linkToLastSubUrl?: boolean; - /** - * A url that legacy apps can set to deep link into their applications. - * - * @internalRemarks - * Currently used by the "lastSubUrl" feature legacy/ui/chrome. This should - * be removed once the ApplicationService is implemented and mounting apps. At that - * time, each app can handle opening to the previous location when they are mounted. - * - * @deprecated - */ - readonly url?: string; - /** * Indicates whether or not this app is currently on the screen. * diff --git a/src/core/public/chrome/nav_links/to_nav_link.test.ts b/src/core/public/chrome/nav_links/to_nav_link.test.ts index 23fdabe0f34301..4c319873af804e 100644 --- a/src/core/public/chrome/nav_links/to_nav_link.test.ts +++ b/src/core/public/chrome/nav_links/to_nav_link.test.ts @@ -85,6 +85,38 @@ describe('toNavLink', () => { expect(link.properties.baseUrl).toEqual('http://localhost/base-path/my-route/my-path'); }); + it('generates the `url` property', () => { + let link = toNavLink( + app({ + appRoute: '/my-route/my-path', + }), + basePath + ); + expect(link.properties.url).toEqual('http://localhost/base-path/my-route/my-path'); + + link = toNavLink( + app({ + appRoute: '/my-route/my-path', + defaultPath: 'some/default/path', + }), + basePath + ); + expect(link.properties.url).toEqual( + 'http://localhost/base-path/my-route/my-path/some/default/path' + ); + }); + + it('does not generate `url` for legacy app', () => { + const link = toNavLink( + legacyApp({ + appUrl: '/my-legacy-app/#foo', + defaultPath: '/some/default/path', + }), + basePath + ); + expect(link.properties.url).toBeUndefined(); + }); + it('uses appUrl when converting legacy applications', () => { expect( toNavLink( diff --git a/src/core/public/chrome/nav_links/to_nav_link.ts b/src/core/public/chrome/nav_links/to_nav_link.ts index 18e4b7b26b6ba1..f79b1df77f8e1c 100644 --- a/src/core/public/chrome/nav_links/to_nav_link.ts +++ b/src/core/public/chrome/nav_links/to_nav_link.ts @@ -20,9 +20,11 @@ import { App, AppNavLinkStatus, AppStatus, LegacyApp } from '../../application'; import { IBasePath } from '../../http'; import { NavLinkWrapper } from './nav_link'; +import { appendAppPath } from '../../application/utils'; export function toNavLink(app: App | LegacyApp, basePath: IBasePath): NavLinkWrapper { const useAppStatus = app.navLinkStatus === AppNavLinkStatus.default; + const baseUrl = isLegacyApp(app) ? basePath.prepend(app.appUrl) : basePath.prepend(app.appRoute!); return new NavLinkWrapper({ ...app, hidden: useAppStatus @@ -30,9 +32,12 @@ export function toNavLink(app: App | LegacyApp, basePath: IBasePath): NavLinkWra : app.navLinkStatus === AppNavLinkStatus.hidden, disabled: useAppStatus ? false : app.navLinkStatus === AppNavLinkStatus.disabled, legacy: isLegacyApp(app), - baseUrl: isLegacyApp(app) - ? relativeToAbsolute(basePath.prepend(app.appUrl)) - : relativeToAbsolute(basePath.prepend(app.appRoute!)), + baseUrl: relativeToAbsolute(baseUrl), + ...(isLegacyApp(app) + ? {} + : { + url: relativeToAbsolute(appendAppPath(baseUrl, app.defaultPath)), + }), }); } diff --git a/src/core/public/chrome/ui/header/nav_link.tsx b/src/core/public/chrome/ui/header/nav_link.tsx index 52b59c53b658c0..d97ef477c2ee0c 100644 --- a/src/core/public/chrome/ui/header/nav_link.tsx +++ b/src/core/public/chrome/ui/header/nav_link.tsx @@ -53,7 +53,7 @@ export function euiNavLink( order, tooltip, } = navLink; - let href = navLink.baseUrl; + let href = navLink.url ?? navLink.baseUrl; if (legacy) { href = url && !active ? url : baseUrl; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index b92bb209d26073..af06b207889c22 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -36,6 +36,7 @@ export interface AppBase { capabilities?: Partial; category?: AppCategory; chromeless?: boolean; + defaultPath?: string; euiIconType?: string; icon?: string; id: string; @@ -168,7 +169,7 @@ export enum AppStatus { export type AppUnmount = () => void; // @public -export type AppUpdatableFields = Pick; +export type AppUpdatableFields = Pick; // @public export type AppUpdater = (app: AppBase) => Partial | undefined; @@ -290,7 +291,6 @@ export interface ChromeNavLink { readonly subUrlBase?: string; readonly title: string; readonly tooltip?: string; - // @deprecated readonly url?: string; } diff --git a/test/plugin_functional/test_suites/core_plugins/application_status.ts b/test/plugin_functional/test_suites/core_plugins/application_status.ts index b6d13a5604011f..c384e41851e15f 100644 --- a/test/plugin_functional/test_suites/core_plugins/application_status.ts +++ b/test/plugin_functional/test_suites/core_plugins/application_status.ts @@ -17,6 +17,7 @@ * under the License. */ +import url from 'url'; import expect from '@kbn/expect'; import { AppNavLinkStatus, @@ -26,6 +27,15 @@ import { import { PluginFunctionalProviderContext } from '../../services'; import '../../plugins/core_app_status/public/types'; +const getKibanaUrl = (pathname?: string, search?: string) => + url.format({ + protocol: 'http:', + hostname: process.env.TEST_KIBANA_HOST || 'localhost', + port: process.env.TEST_KIBANA_PORT || '5620', + pathname, + search, + }); + // eslint-disable-next-line import/no-default-export export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) { const PageObjects = getPageObjects(['common']); @@ -97,6 +107,22 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider expect(await testSubjects.exists('appStatusApp')).to.eql(true); }); + it('allows to change the defaultPath of an application', async () => { + let link = await appsMenu.getLink('App Status'); + expect(link!.href).to.eql(getKibanaUrl('/app/app_status')); + + await setAppStatus({ + defaultPath: '/arbitrary/path', + }); + + link = await appsMenu.getLink('App Status'); + expect(link!.href).to.eql(getKibanaUrl('/app/app_status/arbitrary/path')); + + await navigateToApp('app_status'); + expect(await testSubjects.exists('appStatusApp')).to.eql(true); + expect(await browser.getCurrentUrl()).to.eql(getKibanaUrl('/app/app_status/arbitrary/path')); + }); + it('can change the state of the currently mounted app', async () => { await setAppStatus({ status: AppStatus.accessible, From 6e2691358fdb59af717a9aaa2b32887b9cdf2d2a Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 30 Apr 2020 10:28:14 +0200 Subject: [PATCH 21/30] add generic typings for SavedObjectMigrationFn (#63943) * add generic typings for SavedObjectMigrationFn * change default attributes type to unknown * update generated doc * adapt new calls * update generated doc * update migration example * fix merge conflicts --- ...ugin-core-server.savedobjectmigrationfn.md | 32 ++++++++--- ...gin-core-server.savedobjectsanitizeddoc.md | 2 +- ...n-core-server.savedobjectunsanitizeddoc.md | 2 +- src/core/MIGRATION_EXAMPLES.md | 2 +- .../saved_objects/migrations/core/index.ts | 2 +- .../server/saved_objects/migrations/mocks.ts | 43 +++++++++++++++ .../server/saved_objects/migrations/types.ts | 34 ++++++++---- .../saved_objects_service.mock.ts | 2 + .../saved_objects/serialization/types.ts | 8 +-- src/core/server/server.api.md | 6 +- .../dashboard_migrations.test.ts | 31 ++++++----- .../saved_objects/dashboard_migrations.ts | 10 ++-- .../saved_objects/migrate_match_all_query.ts | 2 +- .../saved_objects/migrations_730.test.ts | 14 ++--- .../saved_objects/index_pattern_migrations.ts | 4 +- .../server/saved_objects/search_migrations.ts | 14 ++--- .../server/saved_objects/tsvb_telemetry.ts | 2 +- .../saved_objects/visualization_migrations.ts | 55 ++++++++++--------- .../graph/server/saved_objects/migrations.ts | 2 +- x-pack/plugins/lens/server/migrations.ts | 9 ++- .../saved_objects/migrations/migrate_6x.ts | 2 +- 21 files changed, 177 insertions(+), 101 deletions(-) create mode 100644 src/core/server/saved_objects/migrations/mocks.ts diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationfn.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationfn.md index a502c40db0cd8c..a3294fb0a087ae 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationfn.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationfn.md @@ -9,22 +9,36 @@ A migration function for a [saved object type](./kibana-plugin-core-server.saved Signature: ```typescript -export declare type SavedObjectMigrationFn = (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => SavedObjectUnsanitizedDoc; +export declare type SavedObjectMigrationFn = (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => SavedObjectUnsanitizedDoc; ``` ## Example ```typescript -const migrateProperty: SavedObjectMigrationFn = (doc, { log }) => { - if(doc.attributes.someProp === null) { - log.warn('Skipping migration'); - } else { - doc.attributes.someProp = migrateProperty(doc.attributes.someProp); - } - - return doc; +interface TypeV1Attributes { + someKey: string; + obsoleteProperty: number; } +interface TypeV2Attributes { + someKey: string; + newProperty: string; +} + +const migrateToV2: SavedObjectMigrationFn = (doc, { log }) => { + const { obsoleteProperty, ...otherAttributes } = doc.attributes; + // instead of mutating `doc` we make a shallow copy so that we can use separate types for the input + // and output attributes. We don't need to make a deep copy, we just need to ensure that obsolete + // attributes are not present on the returned doc. + return { + ...doc, + attributes: { + ...otherAttributes, + newProperty: migrate(obsoleteProperty), + }, + }; +}; + ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsanitizeddoc.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsanitizeddoc.md index 6d4e252fe7532e..3f4090619edbfb 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsanitizeddoc.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsanitizeddoc.md @@ -9,5 +9,5 @@ Describes Saved Object documents that have passed through the migration framewor Signature: ```typescript -export declare type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; +export declare type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md index be51400addbbc5..8e2395ee6310d8 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md @@ -9,5 +9,5 @@ Describes Saved Object documents from Kibana < 7.0.0 which don't have a `refe Signature: ```typescript -export declare type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; +export declare type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; ``` diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md index 8c5fe4875aaea2..c91c00bc1aa021 100644 --- a/src/core/MIGRATION_EXAMPLES.md +++ b/src/core/MIGRATION_EXAMPLES.md @@ -957,7 +957,7 @@ const migration = (doc, log) => {...} Would be converted to: ```typescript -const migration: SavedObjectMigrationFn = (doc, { log }) => {...} +const migration: SavedObjectMigrationFn = (doc, { log }) => {...} ``` ### Remarks diff --git a/src/core/server/saved_objects/migrations/core/index.ts b/src/core/server/saved_objects/migrations/core/index.ts index 466d399f653cd6..f7274740ea5fe3 100644 --- a/src/core/server/saved_objects/migrations/core/index.ts +++ b/src/core/server/saved_objects/migrations/core/index.ts @@ -21,5 +21,5 @@ export { DocumentMigrator } from './document_migrator'; export { IndexMigrator } from './index_migrator'; export { buildActiveMappings } from './build_active_mappings'; export { CallCluster } from './call_cluster'; -export { LogFn } from './migration_logger'; +export { LogFn, SavedObjectsMigrationLogger } from './migration_logger'; export { MigrationResult, MigrationStatus } from './migration_coordinator'; diff --git a/src/core/server/saved_objects/migrations/mocks.ts b/src/core/server/saved_objects/migrations/mocks.ts new file mode 100644 index 00000000000000..76a890d26bfa0c --- /dev/null +++ b/src/core/server/saved_objects/migrations/mocks.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectMigrationContext } from './types'; +import { SavedObjectsMigrationLogger } from './core'; + +const createLoggerMock = (): jest.Mocked => { + const mock = { + debug: jest.fn(), + info: jest.fn(), + warning: jest.fn(), + warn: jest.fn(), + }; + + return mock; +}; + +const createContextMock = (): jest.Mocked => { + const mock = { + log: createLoggerMock(), + }; + return mock; +}; + +export const migrationMocks = { + createContext: createContextMock, +}; diff --git a/src/core/server/saved_objects/migrations/types.ts b/src/core/server/saved_objects/migrations/types.ts index 6bc085dde872e3..85f15b4c18b66a 100644 --- a/src/core/server/saved_objects/migrations/types.ts +++ b/src/core/server/saved_objects/migrations/types.ts @@ -26,23 +26,37 @@ import { SavedObjectsMigrationLogger } from './core/migration_logger'; * * @example * ```typescript - * const migrateProperty: SavedObjectMigrationFn = (doc, { log }) => { - * if(doc.attributes.someProp === null) { - * log.warn('Skipping migration'); - * } else { - * doc.attributes.someProp = migrateProperty(doc.attributes.someProp); - * } + * interface TypeV1Attributes { + * someKey: string; + * obsoleteProperty: number; + * } * - * return doc; + * interface TypeV2Attributes { + * someKey: string; + * newProperty: string; * } + * + * const migrateToV2: SavedObjectMigrationFn = (doc, { log }) => { + * const { obsoleteProperty, ...otherAttributes } = doc.attributes; + * // instead of mutating `doc` we make a shallow copy so that we can use separate types for the input + * // and output attributes. We don't need to make a deep copy, we just need to ensure that obsolete + * // attributes are not present on the returned doc. + * return { + * ...doc, + * attributes: { + * ...otherAttributes, + * newProperty: migrate(obsoleteProperty), + * }, + * }; + * }; * ``` * * @public */ -export type SavedObjectMigrationFn = ( - doc: SavedObjectUnsanitizedDoc, +export type SavedObjectMigrationFn = ( + doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext -) => SavedObjectUnsanitizedDoc; +) => SavedObjectUnsanitizedDoc; /** * Migration context provided when invoking a {@link SavedObjectMigrationFn | migration handler} diff --git a/src/core/server/saved_objects/saved_objects_service.mock.ts b/src/core/server/saved_objects/saved_objects_service.mock.ts index 7ba4613c857d7b..4e1f5981d6a410 100644 --- a/src/core/server/saved_objects/saved_objects_service.mock.ts +++ b/src/core/server/saved_objects/saved_objects_service.mock.ts @@ -31,6 +31,7 @@ import { savedObjectsClientProviderMock } from './service/lib/scoped_client_prov import { savedObjectsRepositoryMock } from './service/lib/repository.mock'; import { savedObjectsClientMock } from './service/saved_objects_client.mock'; import { typeRegistryMock } from './saved_objects_type_registry.mock'; +import { migrationMocks } from './migrations/mocks'; import { ServiceStatusLevels } from '../status'; type SavedObjectsServiceContract = PublicMethodsOf; @@ -105,4 +106,5 @@ export const savedObjectsServiceMock = { createSetupContract: createSetupContractMock, createInternalStartContract: createInternalStartContractMock, createStartContract: createStartContractMock, + createMigrationContext: migrationMocks.createContext, }; diff --git a/src/core/server/saved_objects/serialization/types.ts b/src/core/server/saved_objects/serialization/types.ts index a33e16895078ed..acd2c7b5284aa2 100644 --- a/src/core/server/saved_objects/serialization/types.ts +++ b/src/core/server/saved_objects/serialization/types.ts @@ -47,8 +47,8 @@ export interface SavedObjectsRawDocSource { /** * Saved Object base document */ -interface SavedObjectDoc { - attributes: any; +interface SavedObjectDoc { + attributes: T; id?: string; // NOTE: SavedObjectDoc is used for uncreated objects where `id` is optional type: string; namespace?: string; @@ -69,7 +69,7 @@ interface Referencable { * * @public */ -export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; +export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; /** * Describes Saved Object documents that have passed through the migration @@ -77,4 +77,4 @@ export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; * * @public */ -export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; +export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index dc1c9d379d508a..e8b77a8570291e 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1680,7 +1680,7 @@ export interface SavedObjectMigrationContext { } // @public -export type SavedObjectMigrationFn = (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => SavedObjectUnsanitizedDoc; +export type SavedObjectMigrationFn = (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => SavedObjectUnsanitizedDoc; // @public export interface SavedObjectMigrationMap { @@ -1708,7 +1708,7 @@ export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOpti // Warning: (ae-forgotten-export) The symbol "Referencable" needs to be exported by the entry point index.d.ts // // @public -export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; +export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; // @public (undocumented) export interface SavedObjectsBaseOptions { @@ -2311,7 +2311,7 @@ export class SavedObjectTypeRegistry { } // @public -export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; +export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; // @public export type ScopeableRequest = KibanaRequest | LegacyRequest | FakeRequest; diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts index 9829498118cc0a..22ed18f75c652a 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts @@ -18,14 +18,17 @@ */ import { SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { savedObjectsServiceMock } from '../../../../core/server/mocks'; import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations'; +const contextMock = savedObjectsServiceMock.createMigrationContext(); + describe('dashboard', () => { describe('7.0.0', () => { const migration = migrations['7.0.0']; test('skips error on empty object', () => { - expect(migration({} as SavedObjectUnsanitizedDoc)).toMatchInlineSnapshot(` + expect(migration({} as SavedObjectUnsanitizedDoc, contextMock)).toMatchInlineSnapshot(` Object { "references": Array [], } @@ -44,7 +47,7 @@ Object { '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, }; - const migratedDoc = migration(doc); + const migratedDoc = migration(doc, contextMock); expect(migratedDoc).toMatchInlineSnapshot(` Object { "attributes": Object { @@ -83,7 +86,7 @@ Object { '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, }; - const migratedDoc = migration(doc); + const migratedDoc = migration(doc, contextMock); expect(migratedDoc).toMatchInlineSnapshot(` Object { "attributes": Object { @@ -122,7 +125,7 @@ Object { '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, }; - expect(migration(doc)).toMatchInlineSnapshot(` + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` Object { "attributes": Object { "kibanaSavedObjectMeta": Object { @@ -160,7 +163,7 @@ Object { '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, }; - expect(migration(doc)).toMatchInlineSnapshot(` + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` Object { "attributes": Object { "kibanaSavedObjectMeta": Object { @@ -198,7 +201,7 @@ Object { '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, }; - const migratedDoc = migration(doc); + const migratedDoc = migration(doc, contextMock); expect(migratedDoc).toMatchInlineSnapshot(` Object { "attributes": Object { @@ -237,7 +240,7 @@ Object { '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, }; - const migratedDoc = migration(doc); + const migratedDoc = migration(doc, contextMock); expect(migratedDoc).toMatchInlineSnapshot(` Object { "attributes": Object { @@ -291,7 +294,7 @@ Object { '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, }; - const migratedDoc = migration(doc); + const migratedDoc = migration(doc, contextMock); expect(migratedDoc).toMatchInlineSnapshot(` Object { @@ -331,7 +334,7 @@ Object { panelsJSON: 123, }, } as SavedObjectUnsanitizedDoc; - expect(migration(doc)).toMatchInlineSnapshot(` + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` Object { "attributes": Object { "panelsJSON": 123, @@ -349,7 +352,7 @@ Object { panelsJSON: '{123abc}', }, } as SavedObjectUnsanitizedDoc; - expect(migration(doc)).toMatchInlineSnapshot(` + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` Object { "attributes": Object { "panelsJSON": "{123abc}", @@ -367,7 +370,7 @@ Object { panelsJSON: '{}', }, } as SavedObjectUnsanitizedDoc; - expect(migration(doc)).toMatchInlineSnapshot(` + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` Object { "attributes": Object { "panelsJSON": "{}", @@ -385,7 +388,7 @@ Object { panelsJSON: '[{"id":"123"}]', }, } as SavedObjectUnsanitizedDoc; - expect(migration(doc)).toMatchInlineSnapshot(` + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` Object { "attributes": Object { "panelsJSON": "[{\\"id\\":\\"123\\"}]", @@ -403,7 +406,7 @@ Object { panelsJSON: '[{"type":"visualization"}]', }, } as SavedObjectUnsanitizedDoc; - expect(migration(doc)).toMatchInlineSnapshot(` + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` Object { "attributes": Object { "panelsJSON": "[{\\"type\\":\\"visualization\\"}]", @@ -422,7 +425,7 @@ Object { '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, } as SavedObjectUnsanitizedDoc; - const migratedDoc = migration(doc); + const migratedDoc = migration(doc, contextMock); expect(migratedDoc).toMatchInlineSnapshot(` Object { "attributes": Object { diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts index 7c1d0568cd3d71..4f7945d6dd6017 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts @@ -19,7 +19,7 @@ import { get, flow } from 'lodash'; -import { SavedObjectMigrationFn, SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { SavedObjectMigrationFn } from 'kibana/server'; import { migrations730 } from './migrations_730'; import { migrateMatchAllQuery } from './migrate_match_all_query'; import { DashboardDoc700To720 } from '../../common'; @@ -62,7 +62,7 @@ function migrateIndexPattern(doc: DashboardDoc700To720) { doc.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify(searchSource); } -const migrations700: SavedObjectMigrationFn = (doc): DashboardDoc700To720 => { +const migrations700: SavedObjectMigrationFn = (doc): DashboardDoc700To720 => { // Set new "references" attribute doc.references = doc.references || []; @@ -111,7 +111,7 @@ export const dashboardSavedObjectTypeMigrations = { * in that version. So we apply this twice, once with 6.7.2 and once with 7.0.1 while the backport to 6.7 * only contained the 6.7.2 migration and not the 7.0.1 migration. */ - '6.7.2': flow(migrateMatchAllQuery), - '7.0.0': flow<(doc: SavedObjectUnsanitizedDoc) => DashboardDoc700To720>(migrations700), - '7.3.0': flow(migrations730), + '6.7.2': flow>(migrateMatchAllQuery), + '7.0.0': flow>(migrations700), + '7.3.0': flow>(migrations730), }; diff --git a/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts b/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts index 5b8582bf821ef5..db2fbeb2788023 100644 --- a/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts +++ b/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts @@ -21,7 +21,7 @@ import { SavedObjectMigrationFn } from 'kibana/server'; import { get } from 'lodash'; import { DEFAULT_QUERY_LANGUAGE } from '../../../data/common'; -export const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { +export const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); if (searchSourceJSON) { diff --git a/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts b/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts index aa744324428a41..a58df547fa5224 100644 --- a/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts +++ b/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts @@ -17,19 +17,13 @@ * under the License. */ +import { savedObjectsServiceMock } from '../../../../core/server/mocks'; import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations'; import { migrations730 } from './migrations_730'; import { DashboardDoc700To720, DashboardDoc730ToLatest, DashboardDocPre700 } from '../../common'; import { RawSavedDashboardPanel730ToLatest } from '../../common'; -const mockContext = { - log: { - warning: () => {}, - warn: () => {}, - debug: () => {}, - info: () => {}, - }, -}; +const mockContext = savedObjectsServiceMock.createMigrationContext(); test('dashboard migration 7.3.0 migrates filters to query on search source', () => { const doc: DashboardDoc700To720 = { @@ -95,7 +89,7 @@ test('dashboard migration 7.3.0 migrates filters to query on search source when }, }; - const doc700: DashboardDoc700To720 = migrations['7.0.0'](doc); + const doc700 = migrations['7.0.0'](doc, mockContext); const newDoc = migrations['7.3.0'](doc700, mockContext); const parsedSearchSource = JSON.parse(newDoc.attributes.kibanaSavedObjectMeta.searchSourceJSON); @@ -127,7 +121,7 @@ test('dashboard migration works when panelsJSON is missing panelIndex', () => { }, }; - const doc700: DashboardDoc700To720 = migrations['7.0.0'](doc); + const doc700 = migrations['7.0.0'](doc, mockContext); const newDoc = migrations['7.3.0'](doc700, mockContext); const parsedSearchSource = JSON.parse(newDoc.attributes.kibanaSavedObjectMeta.searchSourceJSON); diff --git a/src/plugins/data/server/saved_objects/index_pattern_migrations.ts b/src/plugins/data/server/saved_objects/index_pattern_migrations.ts index 7a16386ea484c8..c64f7361a8cf4f 100644 --- a/src/plugins/data/server/saved_objects/index_pattern_migrations.ts +++ b/src/plugins/data/server/saved_objects/index_pattern_migrations.ts @@ -20,7 +20,7 @@ import { flow, omit } from 'lodash'; import { SavedObjectMigrationFn } from 'kibana/server'; -const migrateAttributeTypeAndAttributeTypeMeta: SavedObjectMigrationFn = doc => ({ +const migrateAttributeTypeAndAttributeTypeMeta: SavedObjectMigrationFn = doc => ({ ...doc, attributes: { ...doc.attributes, @@ -29,7 +29,7 @@ const migrateAttributeTypeAndAttributeTypeMeta: SavedObjectMigrationFn = doc => }, }); -const migrateSubTypeAndParentFieldProperties: SavedObjectMigrationFn = doc => { +const migrateSubTypeAndParentFieldProperties: SavedObjectMigrationFn = doc => { if (!doc.attributes.fields) return doc; const fieldsString = doc.attributes.fields; diff --git a/src/plugins/data/server/saved_objects/search_migrations.ts b/src/plugins/data/server/saved_objects/search_migrations.ts index 45fa5e11e2a3d7..c8ded51193c92b 100644 --- a/src/plugins/data/server/saved_objects/search_migrations.ts +++ b/src/plugins/data/server/saved_objects/search_migrations.ts @@ -21,7 +21,7 @@ import { flow, get } from 'lodash'; import { SavedObjectMigrationFn } from 'kibana/server'; import { DEFAULT_QUERY_LANGUAGE } from '../../common'; -const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { +const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); if (searchSourceJSON) { @@ -55,7 +55,7 @@ const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { return doc; }; -const migrateIndexPattern: SavedObjectMigrationFn = doc => { +const migrateIndexPattern: SavedObjectMigrationFn = doc => { const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); if (typeof searchSourceJSON !== 'string') { return doc; @@ -97,13 +97,13 @@ const migrateIndexPattern: SavedObjectMigrationFn = doc => { return doc; }; -const setNewReferences: SavedObjectMigrationFn = (doc, context) => { +const setNewReferences: SavedObjectMigrationFn = (doc, context) => { doc.references = doc.references || []; // Migrate index pattern return migrateIndexPattern(doc, context); }; -const migrateSearchSortToNestedArray: SavedObjectMigrationFn = doc => { +const migrateSearchSortToNestedArray: SavedObjectMigrationFn = doc => { const sort = get(doc, 'attributes.sort'); if (!sort) return doc; @@ -122,7 +122,7 @@ const migrateSearchSortToNestedArray: SavedObjectMigrationFn = doc => { }; export const searchSavedObjectTypeMigrations = { - '6.7.2': flow(migrateMatchAllQuery), - '7.0.0': flow(setNewReferences), - '7.4.0': flow(migrateSearchSortToNestedArray), + '6.7.2': flow>(migrateMatchAllQuery), + '7.0.0': flow>(setNewReferences), + '7.4.0': flow>(migrateSearchSortToNestedArray), }; diff --git a/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts b/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts index 34922976f22ff5..1e5508b44ee0ec 100644 --- a/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts +++ b/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts @@ -20,7 +20,7 @@ import { flow } from 'lodash'; import { SavedObjectMigrationFn, SavedObjectsType } from 'kibana/server'; -const resetCount: SavedObjectMigrationFn = doc => ({ +const resetCount: SavedObjectMigrationFn = doc => ({ ...doc, attributes: { ...doc.attributes, diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts index 94473e35a942d5..f6455d0c1e43f4 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts @@ -21,7 +21,7 @@ import { SavedObjectMigrationFn } from 'kibana/server'; import { cloneDeep, get, omit, has, flow } from 'lodash'; import { DEFAULT_QUERY_LANGUAGE } from '../../../data/common'; -const migrateIndexPattern: SavedObjectMigrationFn = doc => { +const migrateIndexPattern: SavedObjectMigrationFn = doc => { const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); if (typeof searchSourceJSON !== 'string') { return doc; @@ -64,7 +64,7 @@ const migrateIndexPattern: SavedObjectMigrationFn = doc => { }; // [TSVB] Migrate percentile-rank aggregation (value -> values) -const migratePercentileRankAggregation: SavedObjectMigrationFn = doc => { +const migratePercentileRankAggregation: SavedObjectMigrationFn = doc => { const visStateJSON = get(doc, 'attributes.visState'); let visState; @@ -100,7 +100,7 @@ const migratePercentileRankAggregation: SavedObjectMigrationFn = doc => { }; // [TSVB] Remove stale opperator key -const migrateOperatorKeyTypo: SavedObjectMigrationFn = doc => { +const migrateOperatorKeyTypo: SavedObjectMigrationFn = doc => { const visStateJSON = get(doc, 'attributes.visState'); let visState; @@ -132,7 +132,7 @@ const migrateOperatorKeyTypo: SavedObjectMigrationFn = doc => { }; // Migrate date histogram aggregation (remove customInterval) -const migrateDateHistogramAggregation: SavedObjectMigrationFn = doc => { +const migrateDateHistogramAggregation: SavedObjectMigrationFn = doc => { const visStateJSON = get(doc, 'attributes.visState'); let visState; @@ -174,7 +174,7 @@ const migrateDateHistogramAggregation: SavedObjectMigrationFn = doc => { return doc; }; -const removeDateHistogramTimeZones: SavedObjectMigrationFn = doc => { +const removeDateHistogramTimeZones: SavedObjectMigrationFn = doc => { const visStateJSON = get(doc, 'attributes.visState'); if (visStateJSON) { let visState; @@ -206,7 +206,7 @@ const removeDateHistogramTimeZones: SavedObjectMigrationFn = doc => { // migrate gauge verticalSplit to alignment // https://github.com/elastic/kibana/issues/34636 -const migrateGaugeVerticalSplitToAlignment: SavedObjectMigrationFn = (doc, logger) => { +const migrateGaugeVerticalSplitToAlignment: SavedObjectMigrationFn = (doc, logger) => { const visStateJSON = get(doc, 'attributes.visState'); if (visStateJSON) { @@ -241,7 +241,7 @@ const migrateGaugeVerticalSplitToAlignment: SavedObjectMigrationFn = (doc, logge Path to the series array is thus: attributes.visState. */ -const transformFilterStringToQueryObject: SavedObjectMigrationFn = (doc, logger) => { +const transformFilterStringToQueryObject: SavedObjectMigrationFn = (doc, logger) => { // Migrate filters // If any filters exist and they are a string, we assume it to be lucene and transform the filter into an object accordingly const newDoc = cloneDeep(doc); @@ -325,7 +325,7 @@ const transformFilterStringToQueryObject: SavedObjectMigrationFn = (doc, logger) return newDoc; }; -const transformSplitFiltersStringToQueryObject: SavedObjectMigrationFn = doc => { +const transformSplitFiltersStringToQueryObject: SavedObjectMigrationFn = doc => { // Migrate split_filters in TSVB objects that weren't migrated in 7.3 // If any filters exist and they are a string, we assume them to be lucene syntax and transform the filter into an object accordingly const newDoc = cloneDeep(doc); @@ -370,7 +370,7 @@ const transformSplitFiltersStringToQueryObject: SavedObjectMigrationFn = doc => return newDoc; }; -const migrateFiltersAggQuery: SavedObjectMigrationFn = doc => { +const migrateFiltersAggQuery: SavedObjectMigrationFn = doc => { const visStateJSON = get(doc, 'attributes.visState'); if (visStateJSON) { @@ -402,7 +402,7 @@ const migrateFiltersAggQuery: SavedObjectMigrationFn = doc => { return doc; }; -const replaceMovAvgToMovFn: SavedObjectMigrationFn = (doc, logger) => { +const replaceMovAvgToMovFn: SavedObjectMigrationFn = (doc, logger) => { const visStateJSON = get(doc, 'attributes.visState'); let visState; @@ -450,7 +450,7 @@ const replaceMovAvgToMovFn: SavedObjectMigrationFn = (doc, logger) => { return doc; }; -const migrateFiltersAggQueryStringQueries: SavedObjectMigrationFn = (doc, logger) => { +const migrateFiltersAggQueryStringQueries: SavedObjectMigrationFn = (doc, logger) => { const visStateJSON = get(doc, 'attributes.visState'); if (visStateJSON) { @@ -483,12 +483,12 @@ const migrateFiltersAggQueryStringQueries: SavedObjectMigrationFn = (doc, logger return doc; }; -const addDocReferences: SavedObjectMigrationFn = doc => ({ +const addDocReferences: SavedObjectMigrationFn = doc => ({ ...doc, references: doc.references || [], }); -const migrateSavedSearch: SavedObjectMigrationFn = doc => { +const migrateSavedSearch: SavedObjectMigrationFn = doc => { const savedSearchId = get(doc, 'attributes.savedSearchId'); if (savedSearchId && doc.references) { @@ -505,7 +505,7 @@ const migrateSavedSearch: SavedObjectMigrationFn = doc => { return doc; }; -const migrateControls: SavedObjectMigrationFn = doc => { +const migrateControls: SavedObjectMigrationFn = doc => { const visStateJSON = get(doc, 'attributes.visState'); if (visStateJSON) { @@ -536,7 +536,7 @@ const migrateControls: SavedObjectMigrationFn = doc => { return doc; }; -const migrateTableSplits: SavedObjectMigrationFn = doc => { +const migrateTableSplits: SavedObjectMigrationFn = doc => { try { const visState = JSON.parse(doc.attributes.visState); if (get(visState, 'type') !== 'table') { @@ -572,7 +572,7 @@ const migrateTableSplits: SavedObjectMigrationFn = doc => { } }; -const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { +const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); if (searchSourceJSON) { @@ -606,7 +606,7 @@ const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { }; // [TSVB] Default color palette is changing, keep the default for older viz -const migrateTsvbDefaultColorPalettes: SavedObjectMigrationFn = doc => { +const migrateTsvbDefaultColorPalettes: SavedObjectMigrationFn = doc => { const visStateJSON = get(doc, 'attributes.visState'); let visState; @@ -649,27 +649,30 @@ export const visualizationSavedObjectTypeMigrations = { * in that version. So we apply this twice, once with 6.7.2 and once with 7.0.1 while the backport to 6.7 * only contained the 6.7.2 migration and not the 7.0.1 migration. */ - '6.7.2': flow(migrateMatchAllQuery, removeDateHistogramTimeZones), - '7.0.0': flow( + '6.7.2': flow>( + migrateMatchAllQuery, + removeDateHistogramTimeZones + ), + '7.0.0': flow>( addDocReferences, migrateIndexPattern, migrateSavedSearch, migrateControls, migrateTableSplits ), - '7.0.1': flow(removeDateHistogramTimeZones), - '7.2.0': flow( + '7.0.1': flow>(removeDateHistogramTimeZones), + '7.2.0': flow>( migratePercentileRankAggregation, migrateDateHistogramAggregation ), - '7.3.0': flow( + '7.3.0': flow>( migrateGaugeVerticalSplitToAlignment, transformFilterStringToQueryObject, migrateFiltersAggQuery, replaceMovAvgToMovFn ), - '7.3.1': flow(migrateFiltersAggQueryStringQueries), - '7.4.2': flow(transformSplitFiltersStringToQueryObject), - '7.7.0': flow(migrateOperatorKeyTypo), - '7.8.0': flow(migrateTsvbDefaultColorPalettes), + '7.3.1': flow>(migrateFiltersAggQueryStringQueries), + '7.4.2': flow>(transformSplitFiltersStringToQueryObject), + '7.7.0': flow>(migrateOperatorKeyTypo), + '7.8.0': flow>(migrateTsvbDefaultColorPalettes), }; diff --git a/x-pack/plugins/graph/server/saved_objects/migrations.ts b/x-pack/plugins/graph/server/saved_objects/migrations.ts index e77d2ea0fb7c97..beb31d548c6702 100644 --- a/x-pack/plugins/graph/server/saved_objects/migrations.ts +++ b/x-pack/plugins/graph/server/saved_objects/migrations.ts @@ -8,7 +8,7 @@ import { get } from 'lodash'; import { SavedObjectUnsanitizedDoc } from 'kibana/server'; export const graphMigrations = { - '7.0.0': (doc: SavedObjectUnsanitizedDoc) => { + '7.0.0': (doc: SavedObjectUnsanitizedDoc) => { // Set new "references" attribute doc.references = doc.references || []; // Migrate index pattern diff --git a/x-pack/plugins/lens/server/migrations.ts b/x-pack/plugins/lens/server/migrations.ts index 51fcd3b6198c3b..583fba1a4a9992 100644 --- a/x-pack/plugins/lens/server/migrations.ts +++ b/x-pack/plugins/lens/server/migrations.ts @@ -19,7 +19,8 @@ interface XYLayerPre77 { * Removes the `lens_auto_date` subexpression from a stored expression * string. For example: aggConfigs={lens_auto_date aggConfigs="JSON string"} */ -const removeLensAutoDate: SavedObjectMigrationFn = (doc, context) => { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const removeLensAutoDate: SavedObjectMigrationFn = (doc, context) => { const expression: string = doc.attributes?.expression; try { const ast = fromExpression(expression); @@ -73,7 +74,8 @@ const removeLensAutoDate: SavedObjectMigrationFn = (doc, context) => { /** * Adds missing timeField arguments to esaggs in the Lens expression */ -const addTimeFieldToEsaggs: SavedObjectMigrationFn = (doc, context) => { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const addTimeFieldToEsaggs: SavedObjectMigrationFn = (doc, context) => { const expression: string = doc.attributes?.expression; try { @@ -131,7 +133,8 @@ const addTimeFieldToEsaggs: SavedObjectMigrationFn = (doc, context) => { } }; -export const migrations: Record = { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const migrations: Record> = { '7.7.0': doc => { const newDoc = cloneDeep(doc); if (newDoc.attributes?.visualizationType === 'lnsXY') { diff --git a/x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.ts b/x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.ts index b063404f68e4fb..65a810ff94a1f5 100644 --- a/x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.ts +++ b/x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.ts @@ -6,7 +6,7 @@ import { SavedObjectMigrationFn } from 'src/core/server'; -export const migrateToKibana660: SavedObjectMigrationFn = doc => { +export const migrateToKibana660: SavedObjectMigrationFn = doc => { if (!doc.attributes.hasOwnProperty('disabledFeatures')) { doc.attributes.disabledFeatures = []; } From dcc4081bd81d080776f019e98232fe743bd226d2 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 30 Apr 2020 10:34:32 +0200 Subject: [PATCH 22/30] Dashboard url generator to preserve saved filters from destination dashboard (#64767) --- src/plugins/dashboard/public/plugin.tsx | 12 +- .../dashboard/public/url_generator.test.ts | 207 +++++++++++++++++- src/plugins/dashboard/public/url_generator.ts | 39 +++- 3 files changed, 244 insertions(+), 14 deletions(-) diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 5f6b67ee6ad202..7de054f2eaa9c8 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -141,10 +141,14 @@ export class DashboardPlugin if (share) { share.urlGenerators.registerUrlGenerator( - createDirectAccessDashboardLinkGenerator(async () => ({ - appBasePath: (await startServices)[0].application.getUrlForApp('dashboard'), - useHashedUrl: (await startServices)[0].uiSettings.get('state:storeInSessionStorage'), - })) + createDirectAccessDashboardLinkGenerator(async () => { + const [coreStart, , selfStart] = await startServices; + return { + appBasePath: coreStart.application.getUrlForApp('dashboard'), + useHashedUrl: coreStart.uiSettings.get('state:storeInSessionStorage'), + savedDashboardLoader: selfStart.getSavedDashboardLoader(), + }; + }) ); } diff --git a/src/plugins/dashboard/public/url_generator.test.ts b/src/plugins/dashboard/public/url_generator.test.ts index d48aacc1d8c1e4..248a3f991d6cbf 100644 --- a/src/plugins/dashboard/public/url_generator.test.ts +++ b/src/plugins/dashboard/public/url_generator.test.ts @@ -21,10 +21,33 @@ import { createDirectAccessDashboardLinkGenerator } from './url_generator'; import { hashedItemStore } from '../../kibana_utils/public'; // eslint-disable-next-line import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock'; -import { esFilters } from '../../data/public'; +import { esFilters, Filter } from '../../data/public'; +import { SavedObjectLoader } from '../../saved_objects/public'; const APP_BASE_PATH: string = 'xyz/app/kibana'; +const createMockDashboardLoader = ( + dashboardToFilters: { + [dashboardId: string]: () => Filter[]; + } = {} +) => { + return { + get: async (dashboardId: string) => { + return { + searchSource: { + getField: (field: string) => { + if (field === 'filter') + return dashboardToFilters[dashboardId] ? dashboardToFilters[dashboardId]() : []; + throw new Error( + `createMockDashboardLoader > searchSource > getField > ${field} is not mocked` + ); + }, + }, + }; + }, + } as SavedObjectLoader; +}; + describe('dashboard url generator', () => { beforeEach(() => { // @ts-ignore @@ -33,7 +56,11 @@ describe('dashboard url generator', () => { test('creates a link to a saved dashboard', async () => { const generator = createDirectAccessDashboardLinkGenerator(() => - Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false }) + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader(), + }) ); const url = await generator.createUrl!({}); expect(url).toMatchInlineSnapshot(`"xyz/app/kibana#/dashboard?_a=()&_g=()"`); @@ -41,7 +68,11 @@ describe('dashboard url generator', () => { test('creates a link with global time range set up', async () => { const generator = createDirectAccessDashboardLinkGenerator(() => - Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false }) + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader(), + }) ); const url = await generator.createUrl!({ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, @@ -53,7 +84,11 @@ describe('dashboard url generator', () => { test('creates a link with filters, time range, refresh interval and query to a saved object', async () => { const generator = createDirectAccessDashboardLinkGenerator(() => - Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false }) + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader(), + }) ); const url = await generator.createUrl!({ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, @@ -89,7 +124,11 @@ describe('dashboard url generator', () => { test('if no useHash setting is given, uses the one was start services', async () => { const generator = createDirectAccessDashboardLinkGenerator(() => - Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: true }) + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: true, + savedDashboardLoader: createMockDashboardLoader(), + }) ); const url = await generator.createUrl!({ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, @@ -99,7 +138,11 @@ describe('dashboard url generator', () => { test('can override a false useHash ui setting', async () => { const generator = createDirectAccessDashboardLinkGenerator(() => - Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false }) + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader(), + }) ); const url = await generator.createUrl!({ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, @@ -110,7 +153,11 @@ describe('dashboard url generator', () => { test('can override a true useHash ui setting', async () => { const generator = createDirectAccessDashboardLinkGenerator(() => - Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: true }) + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: true, + savedDashboardLoader: createMockDashboardLoader(), + }) ); const url = await generator.createUrl!({ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, @@ -118,4 +165,150 @@ describe('dashboard url generator', () => { }); expect(url.indexOf('relative')).toBeGreaterThan(1); }); + + describe('preserving saved filters', () => { + const savedFilter1 = { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { query: 'savedfilter1' }, + }; + + const savedFilter2 = { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { query: 'savedfilter2' }, + }; + + const appliedFilter = { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { query: 'appliedfilter' }, + }; + + test('attaches filters from destination dashboard', async () => { + const generator = createDirectAccessDashboardLinkGenerator(() => + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader({ + ['dashboard1']: () => [savedFilter1], + ['dashboard2']: () => [savedFilter2], + }), + }) + ); + + const urlToDashboard1 = await generator.createUrl!({ + dashboardId: 'dashboard1', + filters: [appliedFilter], + }); + + expect(urlToDashboard1).toEqual(expect.stringContaining('query:savedfilter1')); + expect(urlToDashboard1).toEqual(expect.stringContaining('query:appliedfilter')); + + const urlToDashboard2 = await generator.createUrl!({ + dashboardId: 'dashboard2', + filters: [appliedFilter], + }); + + expect(urlToDashboard2).toEqual(expect.stringContaining('query:savedfilter2')); + expect(urlToDashboard2).toEqual(expect.stringContaining('query:appliedfilter')); + }); + + test("doesn't fail if can't retrieve filters from destination dashboard", async () => { + const generator = createDirectAccessDashboardLinkGenerator(() => + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader({ + ['dashboard1']: () => { + throw new Error('Not found'); + }, + }), + }) + ); + + const url = await generator.createUrl!({ + dashboardId: 'dashboard1', + filters: [appliedFilter], + }); + + expect(url).not.toEqual(expect.stringContaining('query:savedfilter1')); + expect(url).toEqual(expect.stringContaining('query:appliedfilter')); + }); + + test('can enforce empty filters', async () => { + const generator = createDirectAccessDashboardLinkGenerator(() => + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader({ + ['dashboard1']: () => [savedFilter1], + }), + }) + ); + + const url = await generator.createUrl!({ + dashboardId: 'dashboard1', + filters: [], + preserveSavedFilters: false, + }); + + expect(url).not.toEqual(expect.stringContaining('query:savedfilter1')); + expect(url).not.toEqual(expect.stringContaining('query:appliedfilter')); + expect(url).toMatchInlineSnapshot( + `"xyz/app/kibana#/dashboard/dashboard1?_a=(filters:!())&_g=(filters:!())"` + ); + }); + + test('no filters in result url if no filters applied', async () => { + const generator = createDirectAccessDashboardLinkGenerator(() => + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader({ + ['dashboard1']: () => [savedFilter1], + }), + }) + ); + + const url = await generator.createUrl!({ + dashboardId: 'dashboard1', + }); + expect(url).not.toEqual(expect.stringContaining('filters')); + expect(url).toMatchInlineSnapshot(`"xyz/app/kibana#/dashboard/dashboard1?_a=()&_g=()"`); + }); + + test('can turn off preserving filters', async () => { + const generator = createDirectAccessDashboardLinkGenerator(() => + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader({ + ['dashboard1']: () => [savedFilter1], + }), + }) + ); + const urlWithPreservedFiltersTurnedOff = await generator.createUrl!({ + dashboardId: 'dashboard1', + filters: [appliedFilter], + preserveSavedFilters: false, + }); + + expect(urlWithPreservedFiltersTurnedOff).not.toEqual( + expect.stringContaining('query:savedfilter1') + ); + expect(urlWithPreservedFiltersTurnedOff).toEqual( + expect.stringContaining('query:appliedfilter') + ); + }); + }); }); diff --git a/src/plugins/dashboard/public/url_generator.ts b/src/plugins/dashboard/public/url_generator.ts index 0fdf395e75bcaa..6f121ceb2d3731 100644 --- a/src/plugins/dashboard/public/url_generator.ts +++ b/src/plugins/dashboard/public/url_generator.ts @@ -27,6 +27,7 @@ import { } from '../../data/public'; import { setStateToKbnUrl } from '../../kibana_utils/public'; import { UrlGeneratorsDefinition, UrlGeneratorState } from '../../share/public'; +import { SavedObjectLoader } from '../../saved_objects/public'; export const STATE_STORAGE_KEY = '_a'; export const GLOBAL_STATE_STORAGE_KEY = '_g'; @@ -64,10 +65,22 @@ export type DashboardAppLinkGeneratorState = UrlGeneratorState<{ * whether to hash the data in the url to avoid url length issues. */ useHash?: boolean; + + /** + * When `true` filters from saved filters from destination dashboard as merged with applied filters + * When `false` applied filters take precedence and override saved filters + * + * true is default + */ + preserveSavedFilters?: boolean; }>; export const createDirectAccessDashboardLinkGenerator = ( - getStartServices: () => Promise<{ appBasePath: string; useHashedUrl: boolean }> + getStartServices: () => Promise<{ + appBasePath: string; + useHashedUrl: boolean; + savedDashboardLoader: SavedObjectLoader; + }> ): UrlGeneratorsDefinition => ({ id: DASHBOARD_APP_URL_GENERATOR, createUrl: async state => { @@ -76,6 +89,19 @@ export const createDirectAccessDashboardLinkGenerator = ( const appBasePath = startServices.appBasePath; const hash = state.dashboardId ? `dashboard/${state.dashboardId}` : `dashboard`; + const getSavedFiltersFromDestinationDashboardIfNeeded = async (): Promise => { + if (state.preserveSavedFilters === false) return []; + if (!state.dashboardId) return []; + try { + const dashboard = await startServices.savedDashboardLoader.get(state.dashboardId); + return dashboard?.searchSource?.getField('filter') ?? []; + } catch (e) { + // in case dashboard is missing, built the url without those filters + // dashboard app will handle redirect to landing page with toast message + return []; + } + }; + const cleanEmptyKeys = (stateObj: Record) => { Object.keys(stateObj).forEach(key => { if (stateObj[key] === undefined) { @@ -85,11 +111,18 @@ export const createDirectAccessDashboardLinkGenerator = ( return stateObj; }; + // leave filters `undefined` if no filters was applied + // in this case dashboard will restore saved filters on its own + const filters = state.filters && [ + ...(await getSavedFiltersFromDestinationDashboardIfNeeded()), + ...state.filters, + ]; + const appStateUrl = setStateToKbnUrl( STATE_STORAGE_KEY, cleanEmptyKeys({ query: state.query, - filters: state.filters?.filter(f => !esFilters.isFilterPinned(f)), + filters: filters?.filter(f => !esFilters.isFilterPinned(f)), }), { useHash }, `${appBasePath}#/${hash}` @@ -99,7 +132,7 @@ export const createDirectAccessDashboardLinkGenerator = ( GLOBAL_STATE_STORAGE_KEY, cleanEmptyKeys({ time: state.timeRange, - filters: state.filters?.filter(f => esFilters.isFilterPinned(f)), + filters: filters?.filter(f => esFilters.isFilterPinned(f)), refreshInterval: state.refreshInterval, }), { useHash }, From 21bc46f5af1ede7dd209e5a58a224edc37d4b986 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 30 Apr 2020 11:07:14 +0100 Subject: [PATCH 23/30] [ML] Disable data frame anaylics clone button based on permission (#64830) --- .../components/analytics_list/action_clone.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx index 8c65af1d92959f..cc75ddbe08cfb9 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx @@ -18,6 +18,7 @@ import { } from '../../hooks/use_create_analytics_form'; import { State } from '../../hooks/use_create_analytics_form/state'; import { DataFrameAnalyticsListRow } from './common'; +import { checkPermission } from '../../../../../capabilities/check_capabilities'; interface PropDefinition { /** @@ -322,6 +323,8 @@ interface CloneActionProps { * to support EuiContext with a valid DOM structure without nested buttons. */ export const CloneAction: FC = ({ createAnalyticsForm, item }) => { + const canCreateDataFrameAnalytics: boolean = checkPermission('canCreateDataFrameAnalytics'); + const buttonText = i18n.translate('xpack.ml.dataframe.analyticsList.cloneJobButtonLabel', { defaultMessage: 'Clone job', }); @@ -338,6 +341,7 @@ export const CloneAction: FC = ({ createAnalyticsForm, item }) iconType="copy" onClick={onClick} aria-label={buttonText} + disabled={canCreateDataFrameAnalytics === false} > {buttonText}
From 3c56a8e296b4b08c9f73a4924b189b67518a3441 Mon Sep 17 00:00:00 2001 From: Sonja Krause-Harder Date: Thu, 30 Apr 2020 12:43:34 +0200 Subject: [PATCH 24/30] [EPM] Handle constant_keyword type in KB index patterns and ES index templates (#64876) * Unit-test constant_keyword mapping generation * Treat constant_keyword as string in kibana index patterns --- .../elasticsearch/template/template.test.ts | 18 ++++++++++++++++++ .../epm/kibana/index_pattern/install.test.ts | 2 ++ .../epm/kibana/index_pattern/install.ts | 1 + 3 files changed, 21 insertions(+) diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts index 3679c577ee5713..25180244b02147 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts @@ -309,3 +309,21 @@ test('tests processing object field with property, reverse order', () => { const mappings = generateMappings(processedFields); expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldWithPropertyReversedMapping)); }); + +test('tests constant_keyword field type handling', () => { + const constantKeywordLiteralYaml = ` +- name: constantKeyword + type: constant_keyword + `; + const constantKeywordMapping = { + properties: { + constantKeyword: { + type: 'constant_keyword', + }, + }, + }; + const fields: Field[] = safeLoad(constantKeywordLiteralYaml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(JSON.stringify(mappings)).toEqual(JSON.stringify(constantKeywordMapping)); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.test.ts index bc1694348b4c2f..f1660fbc015913 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.test.ts @@ -150,6 +150,7 @@ describe('creating index patterns from yaml fields', () => { { fields: [{ name: 'testField', type: 'text' }], expect: 'string' }, { fields: [{ name: 'testField', type: 'date' }], expect: 'date' }, { fields: [{ name: 'testField', type: 'geo_point' }], expect: 'geo_point' }, + { fields: [{ name: 'testField', type: 'constant_keyword' }], expect: 'string' }, ]; tests.forEach(test => { @@ -191,6 +192,7 @@ describe('creating index patterns from yaml fields', () => { attr: 'aggregatable', }, { fields: [{ name, type: 'keyword' }], expect: true, attr: 'aggregatable' }, + { fields: [{ name, type: 'constant_keyword' }], expect: true, attr: 'aggregatable' }, { fields: [{ name, type: 'text', aggregatable: true }], expect: false, attr: 'aggregatable' }, { fields: [{ name, type: 'text' }], expect: false, attr: 'aggregatable' }, { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts index 05e64c6565dc67..ec657820a22251 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts @@ -47,6 +47,7 @@ const typeMap: TypeMap = { date: 'date', ip: 'ip', boolean: 'boolean', + constant_keyword: 'string', }; export interface IndexPatternField { From b140ad7194338610b791750001730279a4806ecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Thu, 30 Apr 2020 08:56:15 -0400 Subject: [PATCH 25/30] Remove edit alert button from alerts list (#64643) * Remove edit alert button from alerts list * Remove unused code * Cleanup translation files Co-authored-by: Elastic Machine --- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../sections/alert_form/alert_form.test.tsx | 20 ++ .../components/alerts_list.test.tsx | 8 - .../alerts_list/components/alerts_list.tsx | 38 +--- .../apps/triggers_actions_ui/alerts.ts | 187 ------------------ 6 files changed, 21 insertions(+), 234 deletions(-) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 81dc44f3a4cb41..a3011ab5bdfa9e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -15899,7 +15899,6 @@ "xpack.triggersActionsUI.sections.alertsList.actionTypeFilterLabel": "アクションタイプ", "xpack.triggersActionsUI.sections.alertsList.addActionButtonLabel": "アラートの作成", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.alertTypeTitle": "タイプ", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.editLinkTitle": "編集", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.intervalTitle": "次の間隔で実行", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.nameTitle": "名前", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.tagsText": "タグ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e06edb45de8fa9..e373d05a7d8516 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -15904,7 +15904,6 @@ "xpack.triggersActionsUI.sections.alertsList.actionTypeFilterLabel": "操作类型", "xpack.triggersActionsUI.sections.alertsList.addActionButtonLabel": "创建告警", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.alertTypeTitle": "类型", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.editLinkTitle": "编辑", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.intervalTitle": "运行间隔", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.nameTitle": "名称", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.tagsText": "标记", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx index 72c22f46f217e7..8406987e4ed9df 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx @@ -190,5 +190,25 @@ describe('alert_form', () => { const alertTypeSelectOptions = wrapper.find('[data-test-subj="selectedAlertTypeTitle"]'); expect(alertTypeSelectOptions.exists()).toBeTruthy(); }); + + it('should update throttle value', async () => { + const newThrottle = 17; + await setup(); + const throttleField = wrapper.find('[data-test-subj="throttleInput"]'); + expect(throttleField.exists()).toBeTruthy(); + throttleField.at(1).simulate('change', { target: { value: newThrottle.toString() } }); + const throttleFieldAfterUpdate = wrapper.find('[data-test-subj="throttleInput"]'); + expect(throttleFieldAfterUpdate.at(1).prop('value')).toEqual(newThrottle); + }); + + it('should unset throttle value', async () => { + const newThrottle = ''; + await setup(); + const throttleField = wrapper.find('[data-test-subj="throttleInput"]'); + expect(throttleField.exists()).toBeTruthy(); + throttleField.at(1).simulate('change', { target: { value: newThrottle } }); + const throttleFieldAfterUpdate = wrapper.find('[data-test-subj="throttleInput"]'); + expect(throttleFieldAfterUpdate.at(1).prop('value')).toEqual(newThrottle); + }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index 66aa02e1930a3e..a51ebc31267853 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -245,10 +245,6 @@ describe('alerts_list component with items', () => { expect(wrapper.find('EuiBasicTable')).toHaveLength(1); expect(wrapper.find('EuiTableRow')).toHaveLength(2); }); - it('renders edit button for registered alert types', async () => { - await setup(); - expect(wrapper.find('[data-test-subj="alertsTableCell-editLink"]').length).toBeGreaterThan(0); - }); }); describe('alerts_list component empty with show only capability', () => { @@ -442,8 +438,4 @@ describe('alerts_list with show only capability', () => { expect(wrapper.find('EuiTableRow')).toHaveLength(2); // TODO: check delete button }); - it('not renders edit button for non registered alert types', async () => { - await setup(); - expect(wrapper.find('[data-test-subj="alertsTableCell-editLink"]').length).toBe(0); - }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 1103d7c3921a7e..2d9cfcdbda89f8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -24,7 +24,7 @@ import { isEmpty } from 'lodash'; import { AlertsContextProvider } from '../../../context/alerts_context'; import { useAppDependencies } from '../../../app_context'; import { ActionType, Alert, AlertTableItem, AlertTypeIndex, Pagination } from '../../../../types'; -import { AlertAdd, AlertEdit } from '../../alert_form'; +import { AlertAdd } from '../../alert_form'; import { BulkOperationPopover } from '../../common/components/bulk_operation_popover'; import { AlertQuickEditButtonsWithApi as AlertQuickEditButtons } from '../../common/components/alert_quick_edit_buttons'; import { CollapsedItemActionsWithApi as CollapsedItemActions } from './collapsed_item_actions'; @@ -85,8 +85,6 @@ export const AlertsList: React.FunctionComponent = () => { data: [], totalItemCount: 0, }); - const [editedAlertItem, setEditedAlertItem] = useState(undefined); - const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); const [alertsToDelete, setAlertsToDelete] = useState([]); useEffect(() => { @@ -162,11 +160,6 @@ export const AlertsList: React.FunctionComponent = () => { } } - async function editItem(alertTableItem: AlertTableItem) { - setEditedAlertItem(alertTableItem); - setEditFlyoutVisibility(true); - } - const alertsTableColumns = [ { field: 'name', @@ -219,27 +212,6 @@ export const AlertsList: React.FunctionComponent = () => { truncateText: false, 'data-test-subj': 'alertsTableCell-interval', }, - { - name: '', - width: '50px', - render(item: AlertTableItem) { - if (!canSave || !alertTypeRegistry.has(item.alertTypeId)) { - return; - } - return ( - editItem(item)} - > - - - ); - }, - }, { name: '', width: '40px', @@ -453,14 +425,6 @@ export const AlertsList: React.FunctionComponent = () => { addFlyoutVisible={alertFlyoutVisible} setAddFlyoutVisibility={setAlertFlyoutVisibility} /> - {editFlyoutVisible && editedAlertItem ? ( - - ) : null} ); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts index 597f1ad9119b09..9e99c60b4dcb7e 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts @@ -126,193 +126,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ]); }); - it('should edit an alert', async () => { - const createdAlert = await createAlert({ - alertTypeId: '.index-threshold', - name: generateUniqueKey(), - params: { - aggType: 'count', - termSize: 5, - thresholdComparator: '>', - timeWindowSize: 5, - timeWindowUnit: 'm', - groupBy: 'all', - threshold: [1000, 5000], - index: ['.kibana_1'], - timeField: 'alert', - }, - }); - await pageObjects.common.navigateToApp('triggersActions'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - - const searchResults = await pageObjects.triggersActionsUI.getAlertsList(); - expect(searchResults).to.eql([ - { - name: createdAlert.name, - tagsText: 'foo, bar', - alertType: 'Index threshold', - interval: '1m', - }, - ]); - const editLink = await testSubjects.findAll('alertsTableCell-editLink'); - await editLink[0].click(); - - const updatedAlertName = `Changed Alert Name ${generateUniqueKey()}`; - await testSubjects.setValue('alertNameInput', updatedAlertName, { clearWithKeyboard: true }); - - await find.clickByCssSelector('[data-test-subj="saveEditedAlertButton"]:not(disabled)'); - - const toastTitle = await pageObjects.common.closeToast(); - expect(toastTitle).to.eql(`Updated '${updatedAlertName}'`); - await pageObjects.common.navigateToApp('triggersActions'); - await pageObjects.triggersActionsUI.searchAlerts(updatedAlertName); - - const searchResultsAfterEdit = await pageObjects.triggersActionsUI.getAlertsList(); - expect(searchResultsAfterEdit).to.eql([ - { - name: updatedAlertName, - tagsText: 'foo, bar', - alertType: 'Index threshold', - interval: '1m', - }, - ]); - }); - - it('should set an alert throttle', async () => { - const alertName = `edit throttle ${generateUniqueKey()}`; - const createdAlert = await createAlert({ - alertTypeId: '.index-threshold', - name: alertName, - params: { - aggType: 'count', - termSize: 5, - thresholdComparator: '>', - timeWindowSize: 5, - timeWindowUnit: 'm', - groupBy: 'all', - threshold: [1000, 5000], - index: ['.kibana_1'], - timeField: 'alert', - }, - }); - await pageObjects.common.navigateToApp('triggersActions'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - - const searchResults = await pageObjects.triggersActionsUI.getAlertsList(); - expect(searchResults).to.eql([ - { - name: createdAlert.name, - tagsText: 'foo, bar', - alertType: 'Index threshold', - interval: '1m', - }, - ]); - - const editLink = await testSubjects.findAll('alertsTableCell-editLink'); - await editLink[0].click(); - - await testSubjects.setValue('throttleInput', '1', { clearWithKeyboard: true }); - - await find.clickByCssSelector('[data-test-subj="saveEditedAlertButton"]:not(disabled)'); - - expect(await pageObjects.common.closeToast()).to.eql(`Updated '${createdAlert.name}'`); - - await pageObjects.common.navigateToApp('triggersActions'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - await (await testSubjects.findAll('alertsTableCell-editLink'))[0].click(); - const throttleInput = await testSubjects.find('throttleInput'); - expect(await throttleInput.getAttribute('value')).to.eql('1'); - }); - - it('should unset an alert throttle', async () => { - const alertName = `edit throttle ${generateUniqueKey()}`; - const createdAlert = await createAlert({ - alertTypeId: '.index-threshold', - name: alertName, - throttle: '10m', - params: { - aggType: 'count', - termSize: 5, - thresholdComparator: '>', - timeWindowSize: 5, - timeWindowUnit: 'm', - groupBy: 'all', - threshold: [1000, 5000], - index: ['.kibana_1'], - timeField: 'alert', - }, - }); - await pageObjects.common.navigateToApp('triggersActions'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - - const searchResults = await pageObjects.triggersActionsUI.getAlertsList(); - expect(searchResults).to.eql([ - { - name: createdAlert.name, - tagsText: 'foo, bar', - alertType: 'Index threshold', - interval: '1m', - }, - ]); - - const editLink = await testSubjects.findAll('alertsTableCell-editLink'); - await editLink[0].click(); - - const throttleInputToUnsetValue = await testSubjects.find('throttleInput'); - - expect(await throttleInputToUnsetValue.getAttribute('value')).to.eql('10'); - await throttleInputToUnsetValue.click(); - await throttleInputToUnsetValue.clearValueWithKeyboard(); - - expect(await throttleInputToUnsetValue.getAttribute('value')).to.eql(''); - - await find.clickByCssSelector('[data-test-subj="saveEditedAlertButton"]:not(disabled)'); - - expect(await pageObjects.common.closeToast()).to.eql(`Updated '${createdAlert.name}'`); - - await pageObjects.common.navigateToApp('triggersActions'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - await (await testSubjects.findAll('alertsTableCell-editLink'))[0].click(); - const throttleInput = await testSubjects.find('throttleInput'); - expect(await throttleInput.getAttribute('value')).to.eql(''); - }); - - it('should reset alert when canceling an edit', async () => { - const createdAlert = await createAlert({ - alertTypeId: '.index-threshold', - name: generateUniqueKey(), - params: { - aggType: 'count', - termSize: 5, - thresholdComparator: '>', - timeWindowSize: 5, - timeWindowUnit: 'm', - groupBy: 'all', - threshold: [1000, 5000], - index: ['.kibana_1'], - timeField: 'alert', - }, - }); - await pageObjects.common.navigateToApp('triggersActions'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - - const editLink = await testSubjects.findAll('alertsTableCell-editLink'); - await editLink[0].click(); - - const updatedAlertName = `Changed Alert Name ${generateUniqueKey()}`; - await testSubjects.setValue('alertNameInput', updatedAlertName); - - await testSubjects.click('cancelSaveEditedAlertButton'); - await find.waitForDeletedByCssSelector('[data-test-subj="cancelSaveEditedAlertButton"]'); - - const editLinkPostCancel = await testSubjects.findAll('alertsTableCell-editLink'); - await editLinkPostCancel[0].click(); - - const nameInputAfterCancel = await testSubjects.find('alertNameInput'); - const textAfterCancel = await nameInputAfterCancel.getAttribute('value'); - expect(textAfterCancel).to.eql(createdAlert.name); - }); - it('should search for tags', async () => { const createdAlert = await createAlert(); await pageObjects.common.navigateToApp('triggersActions'); From 497c5da763be143d65106e292e9d45bc7496c7b9 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 30 Apr 2020 14:07:32 +0100 Subject: [PATCH 26/30] [ML] Moving get filters capability to admin (#64879) * [ML] Moving get filters capability to admin * updating test --- x-pack/plugins/ml/common/types/capabilities.ts | 4 ++-- .../lib/capabilities/check_capabilities.test.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index da5fd3ac252096..572217ce16eee2 100644 --- a/x-pack/plugins/ml/common/types/capabilities.ts +++ b/x-pack/plugins/ml/common/types/capabilities.ts @@ -15,8 +15,6 @@ export const userMlCapabilities = { canGetCalendars: false, // File Data Visualizer canFindFileStructure: false, - // Filters - canGetFilters: false, // Data Frame Analytics canGetDataFrameAnalytics: false, // Annotations @@ -38,6 +36,8 @@ export const adminMlCapabilities = { canStartStopDatafeed: false, canUpdateDatafeed: false, canPreviewDatafeed: false, + // Filters + canGetFilters: false, // Calendars canCreateCalendar: false, canDeleteCalendar: false, diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts index b6e95ae8373ee1..746c9da47d0adb 100644 --- a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts @@ -64,7 +64,6 @@ describe('check_capabilities', () => { expect(capabilities.canGetDatafeeds).toBe(true); expect(capabilities.canGetCalendars).toBe(true); expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canGetDataFrameAnalytics).toBe(true); expect(capabilities.canGetAnnotations).toBe(true); expect(capabilities.canCreateAnnotation).toBe(true); @@ -81,6 +80,7 @@ describe('check_capabilities', () => { expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); + expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); expect(capabilities.canCreateFilter).toBe(false); @@ -113,7 +113,6 @@ describe('check_capabilities', () => { expect(capabilities.canGetDatafeeds).toBe(true); expect(capabilities.canGetCalendars).toBe(true); expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canGetDataFrameAnalytics).toBe(true); expect(capabilities.canGetAnnotations).toBe(true); expect(capabilities.canCreateAnnotation).toBe(true); @@ -130,6 +129,7 @@ describe('check_capabilities', () => { expect(capabilities.canDeleteDatafeed).toBe(true); expect(capabilities.canUpdateDatafeed).toBe(true); expect(capabilities.canPreviewDatafeed).toBe(true); + expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canCreateCalendar).toBe(true); expect(capabilities.canDeleteCalendar).toBe(true); expect(capabilities.canCreateFilter).toBe(true); @@ -162,7 +162,6 @@ describe('check_capabilities', () => { expect(capabilities.canGetDatafeeds).toBe(true); expect(capabilities.canGetCalendars).toBe(true); expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canGetDataFrameAnalytics).toBe(true); expect(capabilities.canGetAnnotations).toBe(true); expect(capabilities.canCreateAnnotation).toBe(false); @@ -177,6 +176,7 @@ describe('check_capabilities', () => { expect(capabilities.canUpdateJob).toBe(false); expect(capabilities.canCreateDatafeed).toBe(false); expect(capabilities.canDeleteDatafeed).toBe(false); + expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); expect(capabilities.canCreateCalendar).toBe(false); @@ -211,7 +211,6 @@ describe('check_capabilities', () => { expect(capabilities.canGetDatafeeds).toBe(true); expect(capabilities.canGetCalendars).toBe(true); expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canGetDataFrameAnalytics).toBe(true); expect(capabilities.canGetAnnotations).toBe(true); expect(capabilities.canCreateAnnotation).toBe(false); @@ -228,6 +227,7 @@ describe('check_capabilities', () => { expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); + expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); expect(capabilities.canCreateFilter).toBe(false); @@ -260,7 +260,6 @@ describe('check_capabilities', () => { expect(capabilities.canGetDatafeeds).toBe(false); expect(capabilities.canGetCalendars).toBe(false); expect(capabilities.canFindFileStructure).toBe(false); - expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canGetDataFrameAnalytics).toBe(false); expect(capabilities.canGetAnnotations).toBe(false); expect(capabilities.canCreateAnnotation).toBe(false); @@ -277,6 +276,7 @@ describe('check_capabilities', () => { expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); + expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); expect(capabilities.canCreateFilter).toBe(false); @@ -311,7 +311,6 @@ describe('check_capabilities', () => { expect(capabilities.canGetDatafeeds).toBe(false); expect(capabilities.canGetCalendars).toBe(false); expect(capabilities.canFindFileStructure).toBe(false); - expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canGetDataFrameAnalytics).toBe(false); expect(capabilities.canGetAnnotations).toBe(false); expect(capabilities.canCreateAnnotation).toBe(false); @@ -328,6 +327,7 @@ describe('check_capabilities', () => { expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); + expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); expect(capabilities.canCreateFilter).toBe(false); From 60ef51b9122ca6f60590da95f4f6bc75d9f48073 Mon Sep 17 00:00:00 2001 From: Henry Harding Date: Thu, 30 Apr 2020 09:28:12 -0400 Subject: [PATCH 27/30] Feature/send feedback link (#64845) * use fixed table layout * add alpha messaging flyout * Added alpha badge + data streams link * Update x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx Co-Authored-By: Jen Huang * Update x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx Co-Authored-By: Jen Huang * Update x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx Co-Authored-By: Jen Huang * Update x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx Co-Authored-By: Jen Huang * remove small tags * change messaging from alpha to experimental * add period * remove unused imports * fixed i18n ids * add send feedback link to header * rename "ingest management" to "ingest manager" Co-authored-by: Jen Huang Co-authored-by: Elastic Machine --- .../components/settings_flyout.tsx | 2 +- .../ingest_manager/layouts/default.tsx | 28 +++++++++++++++---- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx index 92146e9ee56793..9863463e68a016 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx @@ -209,7 +209,7 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => {

diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx index 10245e73520f75..4a9cfe02b74ace 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx @@ -96,12 +96,28 @@ export const DefaultLayout: React.FunctionComponent = ({ section, childre - setIsSettingsFlyoutOpen(true)}> - - + + + + + + + + setIsSettingsFlyoutOpen(true)}> + + + + From 1736005e5e733212f508fa36ee6d348a00bf6e93 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 30 Apr 2020 15:45:52 +0200 Subject: [PATCH 28/30] [Uptime] Remove hard coded value for monitor states histograms (#64396) --- x-pack/plugins/uptime/common/constants/index.ts | 2 +- x-pack/plugins/uptime/common/constants/query.ts | 7 ------- .../server/lib/requests/search/enrich_monitor_groups.ts | 4 ++-- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/uptime/common/constants/index.ts b/x-pack/plugins/uptime/common/constants/index.ts index 72d498056d6b3c..00baa39044a557 100644 --- a/x-pack/plugins/uptime/common/constants/index.ts +++ b/x-pack/plugins/uptime/common/constants/index.ts @@ -11,6 +11,6 @@ export { CONTEXT_DEFAULTS } from './context_defaults'; export * from './capabilities'; export * from './settings_defaults'; export { PLUGIN } from './plugin'; -export { QUERY, STATES } from './query'; +export { QUERY } from './query'; export * from './ui'; export * from './rest_api'; diff --git a/x-pack/plugins/uptime/common/constants/query.ts b/x-pack/plugins/uptime/common/constants/query.ts index d728f114aae76e..21574f1d8b27e0 100644 --- a/x-pack/plugins/uptime/common/constants/query.ts +++ b/x-pack/plugins/uptime/common/constants/query.ts @@ -25,10 +25,3 @@ export const QUERY = { 'error.type', ], }; - -export const STATES = { - // Number of results returned for a states query - LEGACY_STATES_QUERY_SIZE: 10, - // The maximum number of monitors that should be supported - MAX_MONITORS: 35000, -}; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts index d21259fad77a66..8612d71dfe9394 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts @@ -6,7 +6,7 @@ import { get, sortBy } from 'lodash'; import { QueryContext } from './query_context'; -import { QUERY, STATES } from '../../../../common/constants'; +import { QUERY } from '../../../../common/constants'; import { Check, Histogram, @@ -314,7 +314,7 @@ const getHistogramForMonitors = async ( by_id: { terms: { field: 'monitor.id', - size: STATES.LEGACY_STATES_QUERY_SIZE, + size: queryContext.size, }, aggs: { histogram: { From 027c8a8d752e1f90a133ca5a6c9bd7ca969791c8 Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Thu, 30 Apr 2020 09:47:43 -0400 Subject: [PATCH 29/30] [Endpoint] Remove todos, urls to issues (#64833) --- .../public/applications/endpoint/store/hosts/middleware.ts | 1 - x-pack/plugins/endpoint/server/routes/alerts/list/lib/index.ts | 2 -- x-pack/plugins/endpoint/server/routes/metadata/index.ts | 1 - 3 files changed, 4 deletions(-) diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts index d1b9a2cde4b319..bcfd6b96c9eb88 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts @@ -70,7 +70,6 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = core type: 'serverReturnedHostDetails', payload: response, }); - // FIXME: once we have the API implementation in place, we should call it parallel with the above api call and then dispatch this with the results of the second call dispatch({ type: 'serverReturnedHostPolicyResponse', payload: { diff --git a/x-pack/plugins/endpoint/server/routes/alerts/list/lib/index.ts b/x-pack/plugins/endpoint/server/routes/alerts/list/lib/index.ts index 92bd07a813d264..114251820ce4b4 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/list/lib/index.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/list/lib/index.ts @@ -60,8 +60,6 @@ export const getRequestData = async ( reqData.fromIndex = reqData.pageIndex * reqData.pageSize; } - // See: https://github.com/elastic/elasticsearch-js/issues/662 - // and https://github.com/elastic/endpoint-app-team/issues/221 if ( reqData.searchBefore !== undefined && reqData.searchBefore[0] === '' && diff --git a/x-pack/plugins/endpoint/server/routes/metadata/index.ts b/x-pack/plugins/endpoint/server/routes/metadata/index.ts index 99dc4ac9f9e333..08950930441df0 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/index.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/index.ts @@ -171,7 +171,6 @@ async function enrichHostMetadata( try { /** * Get agent status by elastic agent id if available or use the host id. - * https://github.com/elastic/endpoint-app-team/issues/354 */ if (!elasticAgentId) { From d75328c476aca4afb56090e930a735c43025e7af Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 30 Apr 2020 10:34:44 -0400 Subject: [PATCH 30/30] [Ingest] Allow aggent to send metadata compliant with ECS (#64452) --- .../common/types/models/agent.ts | 11 +++-- .../ingest_manager/components/search_bar.tsx | 1 - .../agent_details_page/components/helper.ts | 47 +++++++++++++++++++ .../components/metadata_flyout.tsx | 6 ++- .../components/metadata_form.tsx | 11 +++-- .../sections/fleet/agent_list_page/index.tsx | 2 +- .../ingest_manager/types/index.ts | 1 + .../ingest_manager/server/saved_objects.ts | 4 +- .../server/services/agents/checkin.ts | 3 +- .../server/services/agents/crud.ts | 2 +- .../server/services/agents/enroll.ts | 4 +- .../server/services/agents/saved_objects.ts | 4 +- .../server/services/agents/status.test.ts | 8 ++-- .../ingest_manager/server/types/index.tsx | 1 + .../es_archives/fleet/agents/data.json | 16 +++---- .../es_archives/fleet/agents/mappings.json | 4 +- 16 files changed, 91 insertions(+), 34 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/helper.ts diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent.ts b/x-pack/plugins/ingest_manager/common/types/models/agent.ts index fcd3955f3a32fc..e3ca7635fdb407 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent.ts @@ -60,6 +60,11 @@ export interface AgentEvent { export interface AgentEventSOAttributes extends AgentEvent, SavedObjectAttributes {} +type MetadataValue = string | AgentMetadata; + +export interface AgentMetadata { + [x: string]: MetadataValue; +} interface AgentBase { type: AgentType; active: boolean; @@ -72,19 +77,17 @@ interface AgentBase { config_revision?: number | null; config_newest_revision?: number; last_checkin?: string; + user_provided_metadata: AgentMetadata; + local_metadata: AgentMetadata; } export interface Agent extends AgentBase { id: string; current_error_events: AgentEvent[]; - user_provided_metadata: Record; - local_metadata: Record; access_api_key?: string; status?: string; } export interface AgentSOAttributes extends AgentBase, SavedObjectAttributes { - user_provided_metadata: string; - local_metadata: string; current_error_events?: string; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx index 4429b9d8e6b829..1c9bd9107515d3 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx @@ -92,7 +92,6 @@ function useSuggestions(fieldPrefix: string, search: string) { const res = (await data.indexPatterns.getFieldsForWildcard({ pattern: INDEX_NAME, })) as IFieldType[]; - if (!data || !data.autocomplete) { throw new Error('Missing data plugin'); } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/helper.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/helper.ts new file mode 100644 index 00000000000000..508190fef0fc2b --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/helper.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AgentMetadata } from '../../../../types'; + +export function flattenMetadata(metadata: AgentMetadata) { + return Object.entries(metadata).reduce((acc, [key, value]) => { + if (typeof value === 'string') { + acc[key] = value; + + return acc; + } + + Object.entries(flattenMetadata(value)).forEach(([flattenedKey, flattenedValue]) => { + acc[`${key}.${flattenedKey}`] = flattenedValue; + }); + + return acc; + }, {} as { [k: string]: string }); +} +export function unflattenMetadata(flattened: { [k: string]: string }) { + const metadata: AgentMetadata = {}; + + Object.entries(flattened).forEach(([flattenedKey, flattenedValue]) => { + const keyParts = flattenedKey.split('.'); + const lastKey = keyParts.pop(); + + if (!lastKey) { + throw new Error('Invalid metadata'); + } + + let metadataPart = metadata; + keyParts.forEach(keyPart => { + if (!metadataPart[keyPart]) { + metadataPart[keyPart] = {}; + } + + metadataPart = metadataPart[keyPart] as AgentMetadata; + }); + metadataPart[lastKey] = flattenedValue; + }); + + return metadata; +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_flyout.tsx index ee43385e601c28..aa6da36f8fb6c7 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_flyout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_flyout.tsx @@ -17,11 +17,13 @@ import { } from '@elastic/eui'; import { MetadataForm } from './metadata_form'; import { Agent } from '../../../../types'; +import { flattenMetadata } from './helper'; interface Props { agent: Agent; flyout: { hide: () => void }; } + export const AgentMetadataFlyout: React.FunctionComponent = ({ agent, flyout }) => { const mapMetadata = (obj: { [key: string]: string } | undefined) => { return Object.keys(obj || {}).map(key => ({ @@ -30,8 +32,8 @@ export const AgentMetadataFlyout: React.FunctionComponent = ({ agent, fly })); }; - const localItems = mapMetadata(agent.local_metadata); - const userProvidedItems = mapMetadata(agent.user_provided_metadata); + const localItems = mapMetadata(flattenMetadata(agent.local_metadata)); + const userProvidedItems = mapMetadata(flattenMetadata(agent.user_provided_metadata)); return ( flyout.hide()} size="s" aria-labelledby="flyoutTitle"> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_form.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_form.tsx index ce28bbdc590b06..af7e8c674db4c0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_form.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_form.tsx @@ -22,6 +22,7 @@ import { useAgentRefresh } from '../hooks'; import { useInput, sendRequest } from '../../../../hooks'; import { Agent } from '../../../../types'; import { agentRouteService } from '../../../../services'; +import { flattenMetadata, unflattenMetadata } from './helper'; function useAddMetadataForm(agent: Agent, done: () => void) { const refreshAgent = useAgentRefresh(); @@ -66,15 +67,17 @@ function useAddMetadataForm(agent: Agent, done: () => void) { isLoading: true, }); + const metadata = unflattenMetadata({ + ...flattenMetadata(agent.user_provided_metadata), + [keyInput.value]: valueInput.value, + }); + try { const { error } = await sendRequest({ path: agentRouteService.getUpdatePath(agent.id), method: 'put', body: JSON.stringify({ - user_provided_metadata: { - ...agent.user_provided_metadata, - [keyInput.value]: valueInput.value, - }, + user_provided_metadata: metadata, }), }); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx index 23fe18b82468cb..05264c157434e0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx @@ -238,7 +238,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { const columns = [ { - field: 'local_metadata.host', + field: 'local_metadata.host.hostname', name: i18n.translate('xpack.ingestManager.agentList.hostColumnTitle', { defaultMessage: 'Host', }), diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts index 1508f4dfaa628b..d6483479a3f2fe 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts @@ -8,6 +8,7 @@ export { entries, // Object types Agent, + AgentMetadata, AgentConfig, NewAgentConfig, AgentEvent, diff --git a/x-pack/plugins/ingest_manager/server/saved_objects.ts b/x-pack/plugins/ingest_manager/server/saved_objects.ts index 90fe68e61bb1b4..89d8b9e173ffe0 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects.ts @@ -56,8 +56,8 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { enrolled_at: { type: 'date' }, access_api_key_id: { type: 'keyword' }, version: { type: 'keyword' }, - user_provided_metadata: { type: 'text' }, - local_metadata: { type: 'text' }, + user_provided_metadata: { type: 'flattened' }, + local_metadata: { type: 'flattened' }, config_id: { type: 'keyword' }, last_updated: { type: 'date' }, last_checkin: { type: 'date' }, diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts index c96a81ed9b7587..9b1565e7d74aab 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts @@ -11,6 +11,7 @@ import { AgentAction, AgentSOAttributes, AgentEventSOAttributes, + AgentMetadata, } from '../../types'; import { agentConfigService } from '../agent_config'; @@ -28,7 +29,7 @@ export async function agentCheckin( const updateData: { last_checkin: string; default_api_key?: string; - local_metadata?: string; + local_metadata?: AgentMetadata; current_error_events?: string; } = { last_checkin: new Date().toISOString(), diff --git a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts index 175b92b75aca05..43fd5a3ce0ac94 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts @@ -103,7 +103,7 @@ export async function updateAgent( } ) { await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { - user_provided_metadata: JSON.stringify(data.userProvidedMetatada), + user_provided_metadata: data.userProvidedMetatada, }); } diff --git a/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts b/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts index a34d2e03e9b3de..81afa70ecb818f 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts @@ -32,8 +32,8 @@ export async function enroll( config_id: configId, type, enrolled_at: enrolledAt, - user_provided_metadata: JSON.stringify(metadata?.userProvided ?? {}), - local_metadata: JSON.stringify(metadata?.local ?? {}), + user_provided_metadata: metadata?.userProvided ?? {}, + local_metadata: metadata?.local ?? {}, current_error_events: undefined, access_api_key_id: undefined, last_checkin: undefined, diff --git a/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts b/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts index b182662e0fb4e2..11beba1cd7e43e 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts @@ -19,8 +19,8 @@ export function savedObjectToAgent(so: SavedObject): Agent { current_error_events: so.attributes.current_error_events ? JSON.parse(so.attributes.current_error_events) : [], - local_metadata: JSON.parse(so.attributes.local_metadata), - user_provided_metadata: JSON.parse(so.attributes.user_provided_metadata), + local_metadata: so.attributes.local_metadata, + user_provided_metadata: so.attributes.user_provided_metadata, access_api_key: undefined, status: undefined, }; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts index b6de083cbe0cb2..8140b1e6de470f 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts @@ -18,8 +18,8 @@ describe('Agent status service', () => { type: AGENT_TYPE_PERMANENT, attributes: { active: false, - local_metadata: '{}', - user_provided_metadata: '{}', + local_metadata: {}, + user_provided_metadata: {}, }, } as SavedObject); const status = await getAgentStatusById(mockSavedObjectsClient, 'id'); @@ -33,8 +33,8 @@ describe('Agent status service', () => { type: AGENT_TYPE_PERMANENT, attributes: { active: true, - local_metadata: '{}', - user_provided_metadata: '{}', + local_metadata: {}, + user_provided_metadata: {}, }, } as SavedObject); const status = await getAgentStatusById(mockSavedObjectsClient, 'id'); diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx index a7019ebc0a271f..27ed1de8499871 100644 --- a/x-pack/plugins/ingest_manager/server/types/index.tsx +++ b/x-pack/plugins/ingest_manager/server/types/index.tsx @@ -8,6 +8,7 @@ import { ScopedClusterClient } from 'src/core/server'; export { // Object types Agent, + AgentMetadata, AgentSOAttributes, AgentStatus, AgentType, diff --git a/x-pack/test/functional/es_archives/fleet/agents/data.json b/x-pack/test/functional/es_archives/fleet/agents/data.json index 3fe4f828ba128b..d22e3cd3fecdda 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/data.json +++ b/x-pack/test/functional/es_archives/fleet/agents/data.json @@ -11,8 +11,8 @@ "shared_id": "agent1_filebeat", "config_id": "1", "type": "PERMANENT", - "local_metadata": "{}", - "user_provided_metadata": "{}" + "local_metadata": {}, + "user_provided_metadata": {} } } } @@ -30,8 +30,8 @@ "active": true, "shared_id": "agent2_filebeat", "type": "PERMANENT", - "local_metadata": "{}", - "user_provided_metadata": "{}" + "local_metadata": {}, + "user_provided_metadata": {} } } } @@ -49,8 +49,8 @@ "active": true, "shared_id": "agent3_metricbeat", "type": "PERMANENT", - "local_metadata": "{}", - "user_provided_metadata": "{}" + "local_metadata": {}, + "user_provided_metadata": {} } } } @@ -68,8 +68,8 @@ "active": true, "shared_id": "agent4_metricbeat", "type": "PERMANENT", - "local_metadata": "{}", - "user_provided_metadata": "{}" + "local_metadata": {}, + "user_provided_metadata": {} } } } diff --git a/x-pack/test/functional/es_archives/fleet/agents/mappings.json b/x-pack/test/functional/es_archives/fleet/agents/mappings.json index 5d5d373797d4cf..409cc3c689eafc 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/mappings.json +++ b/x-pack/test/functional/es_archives/fleet/agents/mappings.json @@ -227,7 +227,7 @@ "type": "date" }, "local_metadata": { - "type": "text" + "type": "flattened" }, "shared_id": { "type": "keyword" @@ -239,7 +239,7 @@ "type": "date" }, "user_provided_metadata": { - "type": "text" + "type": "flattened" }, "version": { "type": "keyword"